From 677df273487a50e9674d1fc52bf8ead574f2c18d Mon Sep 17 00:00:00 2001 From: sk22 Date: Sun, 2 Feb 2025 18:27:16 +0100 Subject: [PATCH] feat(route): Add wohnnet.at real estate search engine (#18243) * Add wohnnet.at real estate search engine * fix string concatenation in single line * add leading slash to path --------- --- lib/routes/wohnnet/index.ts | 129 ++++++++++++++++++++++++++++++++ lib/routes/wohnnet/namespace.ts | 8 ++ 2 files changed, 137 insertions(+) create mode 100644 lib/routes/wohnnet/index.ts create mode 100644 lib/routes/wohnnet/namespace.ts diff --git a/lib/routes/wohnnet/index.ts b/lib/routes/wohnnet/index.ts new file mode 100644 index 000000000..b99290d59 --- /dev/null +++ b/lib/routes/wohnnet/index.ts @@ -0,0 +1,129 @@ +import { load } from 'cheerio'; +import ofetch from '@/utils/ofetch'; +import type { Data, DataItem, Route } from '@/types'; + +const FEED_TITLE = 'wohnnet.at' as const; +const FEED_LOGO = 'https://www.wohnnet.at/media/images/wohnnet/icon_192_192.png' as const; +const FEED_LANGUAGE = 'de' as const; +const ROUTE_PATH_PREFIX = '/wohnnet/' as const; +const BASE_URL = 'https://www.wohnnet.at/immobilien/' as const; + +export const route: Route = { + name: 'Immobiliensuche', + path: '/:category/:region/*', + maintainers: ['sk22'], + categories: ['other'], + description: ` +Only returns the first page of search results, allowing you to keep track of +newly added apartments. If you're looking for an apartment, make sure to also +look through the other pages on the website. + +:::tip +Note that the parameter \`&sortierung=neueste-zuerst\` for chronological order +is automatically appended. +::: + +:::tip +To get your query URL, go to https://www.wohnnet.at/immobilien/suche, apply +all desired filters (but at least a category and a region!) and click the +"… Treffer anzeigen" link. From the resulting URL, cut off the +\`https://www.wohnnet.at/immobilien/\` part at the beginning and replace only +the \`?\` (the \`&\`s stay as is!) after the region name with a \`/\`. + +Examples: + +* \`${BASE_URL}mietwohnungen/wien\` + - → \`${ROUTE_PATH_PREFIX}mietwohnungen/wien\` +* \`${BASE_URL}mietwohnungen/wien?unterregionen=g90101\` + - → \`${ROUTE_PATH_PREFIX}mietwohnungen/wien/unterregionen=g90101\` +* \`${BASE_URL}mietwohnungen/wien?unterregionen=g90101&merkmale=balkon\` + - → \`${ROUTE_PATH_PREFIX}mietwohnungen/wien/unterregionen=g90101&merkmale=balkon\` +::: +`, + example: ROUTE_PATH_PREFIX + 'mietwohnungen/wien/unterregionen=g90101--g90201--g90301--g90401--g90501&flaeche=40&preis=-1000', + parameters: { + category: 'Category (`mietwohnungen`, `eigentumswohnungen`, `grundstuecke`, …)', + region: 'Region (`wien`, `oesterreich`, …)', + unterregionen: 'Unterregionen (e.g. `g90101--g90201--g90301`)', + intention: 'Intention (`kauf` | `miete`)', + zimmer: 'Zimmer (at least number, e.g. `2`)', + flaeche: 'Fläche (m², `40-` = at least 40 m², `40-60` = between 40 m² and 60 m²)', + preis: 'Preis (€, `-1000` = at most 1,000 €, `500-1000` = between 500 € and 1,000 €)', + merkmale: 'Merkmale (multiple, delimited by `--`, e.g. `balkon--garten--kurzzeitmiete--moebliert--parkplatz--provisionsfrei--sofort-beziehbar`)', + }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + async handler(ctx) { + const category = ctx.req.param('category'); + const region = ctx.req.param('region'); + + // /wohnnet/mietwohnungen/wien/&preis=… -> preis=…&sortierung=neueste-zuerst + let path = + ctx.req.path.slice(`${ROUTE_PATH_PREFIX}${category}/${region}/`.length) + + // provide chronological fallback sort (wohnnet.at will use the first one) + '&sortierung=neueste-zuerst'; + if (path.startsWith('&')) { + path = path.slice(1); + } + + const link = `${BASE_URL}${category}/${region}/?${path}`; + const response = await ofetch(link); + const $ = load(response); + + const items = $('a:has(> .realty)') + .toArray() + .map((el) => { + const $el = $(el); + const href = $el.attr('href'); + const [title, address] = $el + .find('.realty-detail-title-address') + .text() + .split('\n') + .map((p) => p.trim()) + .filter((p) => p.length); + const price = $el.find('.realty-detail-area-rooms .text-right').text().trim(); + const details = $el + .find('.realty-detail-area-rooms') + .text() + .split('\n') + .map((p) => p.trim()) + .filter((p) => p.length); + const badges = $el + .find('.realty-detail-badges .badge') + .toArray() + .map((b) => $(b).text().trim()); + const agency = $el.find('.realty-detail-agency').text(); + const imgSrc = $el.find('.realty-image img').attr('src'); + + const itemTitle = `${address} · ${price} | ${title}`; + const itemLink = new URL(href ?? '', BASE_URL).href; + const itemDescription = `${details.join(' · ')} | ${badges.join(' · ')} | ${agency}`; + const itemCategories = badges.filter((b) => !b.endsWith(' Bilder')); + const itemImage = imgSrc ? new URL(imgSrc, BASE_URL).href : undefined; + + return { + title: itemTitle, + link: itemLink, + description: itemDescription, + category: itemCategories, + image: itemImage, + // pubDate is not available on wohnnet.at + } satisfies DataItem; + }); + + return { + title: FEED_TITLE, + language: FEED_LANGUAGE, + logo: FEED_LOGO, + allowEmpty: true, + item: items, + link, + } satisfies Data; + }, +}; diff --git a/lib/routes/wohnnet/namespace.ts b/lib/routes/wohnnet/namespace.ts new file mode 100644 index 000000000..df4ed5069 --- /dev/null +++ b/lib/routes/wohnnet/namespace.ts @@ -0,0 +1,8 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'wohnnet.at', + url: 'wohnnet.at', + description: 'Austrian search engine for real estate', + lang: 'de', +};