fixing tests

This commit is contained in:
Christian Kellner
2025-05-26 10:43:13 +02:00
6 changed files with 69 additions and 17 deletions

View File

@@ -4,13 +4,13 @@
Searching an apartment in Germany can be a frustrating task. Not any longer though, as _Fredy_ will take over and will only notify you once new listings have been found that match your requirements. Searching an apartment in Germany can be a frustrating task. Not any longer though, as _Fredy_ will take over and will only notify you once new listings have been found that match your requirements.
_Fredy_ scrapes multiple services (Immonet, Immowelt etc.) and send new listings to you once they become available. The list of available services can easily be extended. For your convenience, _Fredy_ has a UI to help you configure your search jobs. _Fredy_ scrapes multiple services (Immonet, Immowelt etc.) and send new listings to you once they become available. The list of available services can easily be extended. For your convenience, _Fredy_ has a UI to help you configure your search jobs.
If _Fredy_ finds matching results, it will send them to you via Slack, Email, Telegram etc. (More adapters can be configured.) As _Fredy_ stores the listings it has found, new results will not be sent to you twice (and as a side-effect, _Fredy_ can show some statistics). Furthermore, _Fredy_ checks duplicates per scraping so that the same listings are not being sent twice or more when posted on various platforms (which happens more often than one might think). If _Fredy_ finds matching results, it will send them to you via Slack, Email, Telegram etc. (More adapters can be configured.) As _Fredy_ stores the listings it has found, new results will not be sent to you twice (and as a side-effect, _Fredy_ can show some statistics). Furthermore, _Fredy_ checks duplicates per scraping so that the same listings are not being sent twice or more when posted on various platforms (which happens more often than one might think).
<a href="https://www.producthunt.com/posts/fredy-find-real-estates-damn-easy?embed=true&utm_source=badge-featured&utm_medium=badge&utm_source=badge-fredy&#0045;find&#0045;real&#0045;estates&#0045;damn&#0045;easy" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=965690&theme=light&t=1747292331626" alt="Fredy&#0032;&#0045;&#0032;Find&#0032;Real&#0032;Estates&#0032;Damn&#0032;EasY&#0032; - Your&#0032;personal&#0032;real&#0032;estate&#0032;search&#0032;bot | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> <a href="https://www.producthunt.com/posts/fredy-find-real-estates-damn-easy?embed=true&utm_source=badge-featured&utm_medium=badge&utm_source=badge-fredy&#0045;find&#0045;real&#0045;estates&#0045;damn&#0045;easy" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=965690&theme=light&t=1747292331626" alt="Fredy&#0032;&#0045;&#0032;Find&#0032;Real&#0032;Estates&#0032;Damn&#0032;EasY&#0032; - Your&#0032;personal&#0032;real&#0032;estate&#0032;search&#0032;bot | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
# Sponsorship [![](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/orangecoding) # Sponsorship [![](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/orangecoding)
If you like my work, consider becoming a sponsor. I'm not expecting anybody to pay for _Fredy_ or any other Open Source Project I'm maintaining, however keep in mind, I'm doing all of this in my spare time :) Thanks. If you like my work, consider becoming a sponsor. I'm not expecting anybody to pay for _Fredy_ or any other Open Source Project I'm maintaining, however keep in mind, I'm doing all of this in my spare time :) Thanks.
[![JetBrains logo.](https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.svg)](https://jb.gg/OpenSourceSupport) [![JetBrains logo.](https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.svg)](https://jb.gg/OpenSourceSupport)
@@ -20,9 +20,9 @@ _Fredy_ is supported by JetBrains under Open Source Support Program
## Demo ## Demo
If you want to try out _Fredy_, you can access the demo version [here](https://fredy.orange-coding.net) 🤘 If you want to try out _Fredy_, you can access the demo version [here](https://fredy.orange-coding.net) 🤘
## Usage ## Usage
- Make sure to use Node.js 22 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)
@@ -87,16 +87,16 @@ yarn run test
Immoscout has implemented advanced bot detection. In order to work around this, we are using a reversed engineered version of their mobile api. See See [Immoscout Reverse Engineering Documentation](https://github.com/orangecoding/fredy/blob/master/reverse-engineered-immoscout.md) Immoscout has implemented advanced bot detection. In order to work around this, we are using a reversed engineered version of their mobile api. See See [Immoscout Reverse Engineering Documentation](https://github.com/orangecoding/fredy/blob/master/reverse-engineered-immoscout.md)
# Analytics # Analytics
Fredy is completely free (and will always remain free). However, it would be a huge help if youd allow me to collect some analytical data. Fredy is completely free (and will always remain free). However, it would be a huge help if youd allow me to collect some analytical data.
Before you freak out, let me explain... Before you freak out, let me explain...
If you agree, Fredy will send a ping to my Mixpanel project each time it runs. If you agree, Fredy will send a ping to my Mixpanel project each time it runs.
The data includes: names of active adapters/providers, OS, architecture, Node version, and language. The information is entirely anonymous and helps me understand which adapters/providers are most frequently used.</p> The data includes: names of active adapters/providers, OS, architecture, Node version, and language. The information is entirely anonymous and helps me understand which adapters/providers are most frequently used.</p>
**Thanks**🤘 **Thanks**🤘
# Docker # Docker
Use the Dockerfile in this repository to build an image. Use the Dockerfile in this repository to build an image.
Example: `docker build -t fredy/fredy /path/to/your/Dockerfile` Example: `docker build -t fredy/fredy /path/to/your/Dockerfile`
Or use docker-compose: Or use docker-compose:
@@ -106,12 +106,16 @@ Or use the container that will be built automatically.
`docker pull ghcr.io/orangecoding/fredy:master` `docker pull ghcr.io/orangecoding/fredy:master`
## Create & run a container ## Create & run a container
Put your config.json into a path of your choice, such as `/path/to/your/conf/`. Put your config.json into a path of your choice, such as `/path/to/your/conf/`.
Example: `docker create --name fredy -v /path/to/your/conf/:/conf -p 9998:9998 fredy/fredy` Example: `docker create --name fredy -v /path/to/your/conf/:/conf -p 9998:9998 fredy/fredy`
## Logs
You can browse the logs with `docker logs fredy -f`.
### 👐 Contributing ### 👐 Contributing
Thanks to all the people who already contributed! Thanks to all the people who already contributed!
@@ -121,6 +125,7 @@ Thanks to all the people who already contributed!
See [Contributing](https://github.com/orangecoding/fredy/blob/master/CONTRIBUTING.md) See [Contributing](https://github.com/orangecoding/fredy/blob/master/CONTRIBUTING.md)
## Logs
You can browse the logs with `docker logs fredy -f`. ## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=orangecoding/fredy&type=Date)](https://www.star-history.com/#orangecoding/fredy&Date)

View File

@@ -23,7 +23,7 @@ const config = {
id: 'a@href', id: 'a@href',
price: 'div[data-testid="cardmfe-price-testid"] | removeNewline | trim', price: 'div[data-testid="cardmfe-price-testid"] | removeNewline | trim',
size: 'div[data-testid="cardmfe-keyfacts-testid"] | removeNewline | trim', size: 'div[data-testid="cardmfe-keyfacts-testid"] | removeNewline | trim',
title: '.css-jv3zx6', title: 'div[data-testid="cardmfe-description-box-text-test-id"] > div:nth-of-type(2)',
link: 'a@href', link: 'a@href',
address: 'div[data-testid="cardmfe-description-box-address"] | removeNewline | trim', address: 'div[data-testid="cardmfe-description-box-address"] | removeNewline | trim',
}, },

View File

@@ -79,6 +79,7 @@ const PARAM_NAME_MAP = {
geocoordinates: 'geocoordinates', geocoordinates: 'geocoordinates',
shape: 'shape', shape: 'shape',
sorting: 'sorting', sorting: 'sorting',
newbuilding: 'newbuilding',
}; };
const EQUIPMENT_MAP = { const EQUIPMENT_MAP = {
@@ -89,6 +90,7 @@ const EQUIPMENT_MAP = {
garden: 'garden', garden: 'garden',
guesttoilet: 'guestToilet', guesttoilet: 'guestToilet',
balcony: 'balcony', balcony: 'balcony',
handicappedaccessible: 'handicappedAccessible',
}; };
const REAL_ESTATE_TYPE = { const REAL_ESTATE_TYPE = {
@@ -98,6 +100,29 @@ const REAL_ESTATE_TYPE = {
'haus-kaufen': 'housebuy', 'haus-kaufen': 'housebuy',
}; };
const WEB_PATH_TO_APARTMENT_EQUIPMENT_MAP = {
// Category "Balkon/Terrasse"
'wohnung-mit-balkon-mieten': { equipment: ['balcony'] },
'wohnung-mit-garten-mieten': { equipment: ['garden'] },
// Category "Wohnungstyp"
'souterrainwohnung-mieten': { apartmenttypes: ['halfbasement'] },
'erdgeschosswohnung-mieten': { apartmenttypes: ['groundfloor'] },
'hochparterrewohnung-mieten': { apartmenttypes: ['raisedgroundfloor'] },
'etagenwohnung-mieten': { apartmenttypes: ['apartment'] },
'loft-mieten': { apartmenttypes: ['loft'] },
'maisonette-mieten': { apartmenttypes: ['maisonette'] },
'terrassenwohnung-mieten': { apartmenttypes: ['terracedflat'] },
'penthouse-mieten': { apartmenttypes: ['penthouse'] },
'dachgeschosswohnung-mieten': { apartmenttypes: ['roofstorey'] },
// Category "Ausstattung"
'wohnung-mit-garage-mieten': { equipment: ['parking'] },
'wohnung-mit-einbaukueche-mieten': { equipment: ['builtinkitchen'] },
'wohnung-mit-keller-mieten': { equipment: ['cellar'] },
// Category "Merkmale"
'neubauwohnung-mieten': { newbuilding: true },
'barrierefreie-wohnung-mieten': { equipment: ['handicappedaccessible'] },
};
export function convertWebToMobile(webUrl) { export function convertWebToMobile(webUrl) {
let url; let url;
try { try {
@@ -112,9 +137,17 @@ export function convertWebToMobile(webUrl) {
} }
const realTypeKey = segments.at(-1); const realTypeKey = segments.at(-1);
const realType = REAL_ESTATE_TYPE[realTypeKey]; let realType = REAL_ESTATE_TYPE[realTypeKey];
let additionalParamsFromWebPath;
if (!realType) { if (!realType) {
throw new Error(`Real estate type not found: ${realTypeKey}`); // Test for seo optimized apartment path (only used on the ImmoScout web app)
if (WEB_PATH_TO_APARTMENT_EQUIPMENT_MAP[realTypeKey]) {
additionalParamsFromWebPath = WEB_PATH_TO_APARTMENT_EQUIPMENT_MAP[realTypeKey];
realType = REAL_ESTATE_TYPE['wohnung-mieten'];
} else {
throw new Error(`Real estate type not found: ${realTypeKey}`);
}
} }
if (segments.includes('shape')) { if (segments.includes('shape')) {
@@ -132,6 +165,7 @@ export function convertWebToMobile(webUrl) {
searchType: isRadius ? 'radius' : 'region', searchType: isRadius ? 'radius' : 'region',
realestatetype: realType, realestatetype: realType,
...(isRadius ? {} : { geocodes }), ...(isRadius ? {} : { geocodes }),
...additionalParamsFromWebPath,
}; };
if (webParams.geocoordinates) { if (webParams.geocoordinates) {
@@ -141,7 +175,11 @@ export function convertWebToMobile(webUrl) {
for (const [key, val] of Object.entries(webParams)) { for (const [key, val] of Object.entries(webParams)) {
if (key === 'equipment') { if (key === 'equipment') {
const items = [].concat(val).flatMap((v) => `${v}`.split(',')); const items = [].concat(val).flatMap((v) => `${v}`.split(','));
mobileParams[PARAM_NAME_MAP[key]] = items.map((item) => EQUIPMENT_MAP[item.toLowerCase()]).filter(Boolean); const currentEquipmentParams = mobileParams[PARAM_NAME_MAP[key]];
mobileParams[PARAM_NAME_MAP[key]] = [
...(currentEquipmentParams ?? []),
...items.map((item) => EQUIPMENT_MAP[item.toLowerCase()]).filter(Boolean),
];
} else { } else {
mobileParams[PARAM_NAME_MAP[key]] = val; mobileParams[PARAM_NAME_MAP[key]] = val;
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "fredy", "name": "fredy",
"version": "12.0.0", "version": "11.2.1",
"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 prod.js", "start": "node prod.js",

View File

@@ -37,7 +37,7 @@
"enabled": true "enabled": true
}, },
"wgGesucht": { "wgGesucht": {
"url": "https://www.wg-gesucht.de/wg-zimmer-in-Duesseldorf.30.0.1.0.html?offer_filter=1&noDeact=1&city_id=30&category=0&rent_type=0&rMax=5000", "url": "https://www.wg-gesucht.de/wg-zimmer-in-Duesseldorf.30.0.1.0.html",
"enabled": true "enabled": true
} }
} }

View File

@@ -16,6 +16,15 @@ describe('#immoscout-mobile URL conversion', () => {
expect(actualMobileUrl).to.equal(expectedMobileUrl); expect(actualMobileUrl).to.equal(expectedMobileUrl);
}); });
// Test URL conversion of web-only SEO path
it('should convert a SEO web path to the correct query params', () => {
const webUrl = 'https://www.immobilienscout24.de/Suche/de/berlin/berlin/wohnung-mit-balkon-mieten?equipment=garden';
const converted = convertWebToMobile(webUrl);
const queryParams = new URL(converted).searchParams;
expect(queryParams.get('equipment').split(',')).to.include.members(['garden', 'balcony']);
});
// Test URL conversion with unsupported query parameters // Test URL conversion with unsupported query parameters
it('should remove unsupported query parameters', () => { it('should remove unsupported query parameters', () => {
const webUrl = 'https://www.immobilienscout24.de/Suche/de/berlin/berlin/wohnung-mieten?minimuminternetspeed=100000'; const webUrl = 'https://www.immobilienscout24.de/Suche/de/berlin/berlin/wohnung-mieten?minimuminternetspeed=100000';