Compare commits

...

31 Commits

Author SHA1 Message Date
Christian Kellner
2fd03bce79 improve docker build 2025-05-26 13:20:12 +02:00
Christian Kellner
78a122b3ea improve docker build 2025-05-26 12:07:22 +02:00
Christian Kellner
918c6ade36 next version 2025-05-26 11:57:54 +02:00
Christian Kellner
9fac1aee06 adding forgotten yarn.lock 2025-05-26 11:34:05 +02:00
Christian Kellner
f9c6b10976 fixing tests 2025-05-26 10:43:13 +02:00
Christian Kellner
d8ccccb82a Next version of fredy 2025-05-20 12:45:12 +02:00
Leon C.
1f54bcfd3f ImmoScout: Allow web paths with SEO optimization to be filtered to query params (#128) 2025-05-20 12:44:43 +02:00
Christian Kellner
f4c2130829 Update README.md 2025-05-17 09:09:42 +02:00
Christian Kellner
d624e70732 adding lock 2025-05-16 15:10:06 +02:00
Christian Kellner
0cbfaaf092 revert to use yarn 2025-05-16 15:03:28 +02:00
Christian Kellner
c6fb856cb6 fix docker build 2025-05-16 14:26:39 +02:00
Christian Kellner
6fe0a9dc3c fix pnpm version 2025-05-16 14:23:01 +02:00
Christian Kellner
5d52e4152d fix pnpm version 2025-05-16 14:21:29 +02:00
Christian Kellner
a8e5f8b524 improve test and docker runner 2025-05-16 14:19:20 +02:00
Christian Kellner
4b45ff4430 improve readme 2025-05-16 14:06:02 +02:00
Christian Kellner
db6211777b improve test and docker runner 2025-05-16 14:04:55 +02:00
Christian Kellner
21dd48527c fixing test runner 2025-05-16 14:00:17 +02:00
Christian Kellner
b0d494eed6 ading lock 2025-05-16 13:58:45 +02:00
Christian Kellner
9efb3e4b94 tagging new version, switching to node 22 2025-05-16 13:45:29 +02:00
Christian Kellner
683c47f61c tagging new version, switching to node 22 2025-05-16 13:44:45 +02:00
Christian Kellner
b3c11320d4 switching to pnpm for faster build 2025-05-16 13:38:25 +02:00
Christian Kellner
25dfad4f5d run cleanup once at start 2025-05-16 13:26:39 +02:00
Christian Kellner
b7a3823049 console log when removing demo jobs 2025-05-16 13:25:55 +02:00
Christian Kellner
6964998695 fixing removing demo jobs 2025-05-16 13:20:54 +02:00
Christian Kellner
ef689cf97e fix docker build harder 2025-05-15 10:37:21 +02:00
Christian Kellner
bd6a572ab0 fix docker build 2025-05-15 10:23:56 +02:00
Christian Kellner
d96c1ee3fe Merge branch 'master' of github.com:orangecoding/fredy 2025-05-15 10:16:55 +02:00
Christian Kellner
9a09548a07 fix docker build 2025-05-15 10:16:43 +02:00
Christian Kellner
00eabecd08 Update README.md 2025-05-15 09:00:17 +02:00
Christian Kellner
c07dc6220e Update README.md 2025-05-14 15:05:50 +02:00
Christian Kellner
4bab3bd9da fix docker build 2025-05-14 14:28:07 +02:00
13 changed files with 1290 additions and 2685 deletions

View File

@@ -1,7 +1,6 @@
node_modules/ node_modules/
npm-debug.log npm-debug.log
test/ test/
conf/
db/ db/
.git/ .git/
.github/ .github/

View File

@@ -1,4 +1,5 @@
name: Create and publish Docker image name: Create and publish Docker image
on: on:
push: push:
branches: branches:
@@ -17,15 +18,24 @@ jobs:
contents: read contents: read
packages: write packages: write
steps: concurrency:
- name: Set up Docker Buildx group: ${{ github.workflow }}-${{ github.ref }}
uses: docker/setup-buildx-action@v1 cancel-in-progress: true
steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
with:
platforms: linux/amd64,linux/arm64
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Log in to the Container registry - name: Log in to the Container registry
uses: docker/login-action@v1 uses: docker/login-action@v2
with: with:
registry: ${{ env.REGISTRY }} registry: ${{ env.REGISTRY }}
username: ${{ github.actor }} username: ${{ github.actor }}
@@ -33,15 +43,17 @@ jobs:
- name: Extract metadata (tags, labels) for Docker - name: Extract metadata (tags, labels) for Docker
id: meta id: meta
uses: docker/metadata-action@v3 uses: docker/metadata-action@v4
with: with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- name: Build and push Docker image - name: Build and push Docker image
uses: docker/build-push-action@v2 uses: docker/build-push-action@v3
with: with:
context: . context: .
push: true push: true
platforms: linux/amd64,linux/arm64
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 cache-from: type=gha
cache-to: type=gha,mode=max

View File

@@ -113,7 +113,7 @@ I'm using Eslint to maintain quote style and quality. Do not skip it...
##### To do before merging: ##### To do before merging:
- executed tests? (`yarn run test`) - executed tests? (`pnpm test`)
- sure the changes are useful for everybody? Or is it maybe a custom modification just for your case? - sure the changes are useful for everybody? Or is it maybe a custom modification just for your case?
_Thanks!_ :heart: _Thanks!_ :heart:

View File

@@ -1,25 +1,35 @@
FROM node:20 FROM node:22-slim
WORKDIR /fredy WORKDIR /fredy
COPY . /fredy # Install Chromium without extra recommended packages and clean apt cache
RUN apt-get update \
RUN apt-get update && apt-get install -y chromium && apt-get install -y --no-install-recommends chromium \
&& rm -rf /var/lib/apt/lists/*
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true \ ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true \
PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
RUN yarn install # Copy lockfiles first to leverage cache for dependencies
COPY package.json yarn.lock ./
RUN yarn global add pm2 # Set Yarn timeout, install dependencies and PM2 globally
RUN yarn config set network-timeout 600000 \
&& yarn install --frozen-lockfile \
&& yarn global add pm2
# Copy application source and build production assets
COPY . ./
RUN yarn run prod RUN yarn run prod
RUN mkdir /db /conf && \ # Prepare runtime directories and symlinks for data and config
chown 1000:1000 /db /conf && \ RUN mkdir -p /db /conf \
chmod 777 -R /db/ && \ && chown 1000:1000 /db /conf \
ln -s /db /fredy/db && ln -s /conf /fredy/conf && chmod 777 /db /conf \
&& ln -s /db /fredy/db \
&& ln -s /conf /fredy/conf
EXPOSE 9998 EXPOSE 9998
CMD pm2-runtime index.js # Start application using PM2 runtime
CMD ["pm2-runtime", "index.js"]

View File

@@ -1,6 +1,6 @@
<img src="https://github.com/orangecoding/fredy/blob/master/doc/logo.png" width="400"> <img src="https://github.com/orangecoding/fredy/blob/master/doc/logo.png" width="400">
![Build Status](https://github.com/orangecoding/fredy/actions/workflows/test.yml/badge.svg) ![Build Status](https://github.com/orangecoding/fredy/actions/workflows/test.yml/badge.svg) [![Create and publish Docker image](https://github.com/orangecoding/fredy/actions/workflows/docker.yml/badge.svg)](https://github.com/orangecoding/fredy/actions/workflows/docker.yml)
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.
@@ -8,6 +8,8 @@ _Fredy_ scrapes multiple services (Immonet, Immowelt etc.) and send new listings
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>
# 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.
@@ -110,6 +112,10 @@ 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!
@@ -119,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

@@ -1,29 +1,37 @@
import { setInterval } from 'node:timers'; import { setInterval } from 'node:timers';
import {removeJobsByUserName} from './storage/jobStorage.js'; import { removeJobsByUserName } from './storage/jobStorage.js';
import {config} from '../utils.js'; import { config } from '../utils.js';
import { getUsers } from './storage/userStorage.js';
/** /**
* if we are running in demo environment, we have to cleanup the db files (specifically the jobs table) * if we are running in demo environment, we have to cleanup the db files (specifically the jobs table)
*/ */
export function cleanupDemoAtMidnight() { export function cleanupDemoAtMidnight() {
const now = new Date(); const now = new Date();
const millisUntilMidnightUTC = (24 - now.getUTCHours()) * 60 * 60 * 1000 const millisUntilMidnightUTC =
- now.getUTCMinutes() * 60 * 1000 (24 - now.getUTCHours()) * 60 * 60 * 1000 -
- now.getUTCSeconds() * 1000 now.getUTCMinutes() * 60 * 1000 -
- now.getUTCMilliseconds(); now.getUTCSeconds() * 1000 -
now.getUTCMilliseconds();
setTimeout(() => { cleanup();
setTimeout(() => {
setInterval(
() => {
cleanup(); cleanup();
},
setInterval(() => { 24 * 60 * 60 * 1000,
cleanup(); );
}, 24 * 60 * 60 * 1000); }, millisUntilMidnightUTC);
}, millisUntilMidnightUTC);
} }
function cleanup(){ function cleanup() {
if(config.demoMode){ if (config.demoMode) {
removeJobsByUserName('demo'); const demoUser = getUsers(false).find((user) => user.username === 'demo');
if (demoUser == null) {
console.error('Demo user not found, cannot remove Jobs');
return;
} }
} removeJobsByUserName(demoUser.id);
}
}

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

@@ -11,7 +11,6 @@ const db = new LowdashAdapter(adapter, { jobs: [] });
db.read(); db.read();
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 =
jobId == null jobId == null
@@ -77,16 +76,25 @@ export const removeJobsByUserId = (userId) => {
.value(); .value();
db.write(); db.write();
}; };
export const removeJobsByUserName = (userName) => { export const removeJobsByUserName = (userId) => {
let removedDemoJobs = 0;
db.chain db.chain
.get('jobs') .get('jobs')
.filter((job) => job.username === userName) .filter((job) => job.userId === userId)
.forEach((job) => listingStorage.removeListings(job.id)); .forEach((job) => {
removedDemoJobs++;
listingStorage.removeListings(job.id);
});
db.chain db.chain
.get('jobs') .get('jobs')
.remove((job) => job.username === userName) .remove((job) => job.userId === userId)
.value(); .value();
db.write(); db.write();
if (removedDemoJobs > 0) {
/* eslint-disable no-console */
console.log(`Removed ${removedDemoJobs} demo jobs`);
/* eslint-enable no-console */
}
}; };
export const getJobs = () => { export const getJobs = () => {
return db.chain return db.chain

View File

@@ -1,6 +1,6 @@
{ {
"name": "fredy", "name": "fredy",
"version": "11.2.0", "version": "11.2.2",
"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';

3768
yarn.lock

File diff suppressed because it is too large Load Diff