|
9 | 9 |
|
10 | 10 | BASE = "https://www.blocket.se" |
11 | 11 | SEARCH_URL = f"{BASE}/recommerce/forsale/search/api/search/SEARCH_ID_BAP_COMMON" |
| 12 | +CAR_SEARCH_URL = f"{BASE}/mobility/search/api/search/SEARCH_ID_CAR_USED" |
| 13 | +BOAT_SEARCH_URL = f"{BASE}/mobility/search/api/search/SEARCH_ID_BOAT_USED" |
| 14 | +MC_SEARCH_URL = f"{BASE}/mobility/search/api/search/SEARCH_ID_MC_USED" |
12 | 15 |
|
13 | 16 | HEADERS = { |
14 | | - "User-Agent": "blocket-cli/0.1.0 (https://github.com/lennart-johansson/blocket-cli)", |
| 17 | + "User-Agent": "blocket-cli/0.2.0 (https://github.com/lennart-johansson/blocket-cli)", |
15 | 18 | } |
16 | 19 |
|
17 | 20 | LOCATIONS = { |
|
68 | 71 | } |
69 | 72 |
|
70 | 73 |
|
| 74 | +FUELS = { |
| 75 | + "bensin": "1", |
| 76 | + "diesel": "2", |
| 77 | + "el": "4", |
| 78 | + "etanol": "2441", |
| 79 | + "cng": "3", |
| 80 | + "hybrid-bensin": "6", |
| 81 | + "hybrid-diesel": "8", |
| 82 | + "hybrid-gas": "5", |
| 83 | + "plugin-bensin": "1352", |
| 84 | + "plugin-diesel": "1356", |
| 85 | +} |
| 86 | + |
| 87 | +FUEL_LABELS = { |
| 88 | + "bensin": "Bensin", |
| 89 | + "diesel": "Diesel", |
| 90 | + "el": "El", |
| 91 | + "etanol": "Etanol", |
| 92 | + "cng": "CNG", |
| 93 | + "hybrid-bensin": "Hybrid (bensin)", |
| 94 | + "hybrid-diesel": "Hybrid (diesel)", |
| 95 | + "hybrid-gas": "Hybrid (gas)", |
| 96 | + "plugin-bensin": "Plug-in (bensin)", |
| 97 | + "plugin-diesel": "Plug-in (diesel)", |
| 98 | +} |
| 99 | + |
| 100 | +TRANSMISSIONS = { |
| 101 | + "manuell": "1", |
| 102 | + "automat": "2", |
| 103 | +} |
| 104 | + |
| 105 | +TRANSMISSION_LABELS = { |
| 106 | + "manuell": "Manuell", |
| 107 | + "automat": "Automatisk", |
| 108 | +} |
| 109 | + |
| 110 | + |
| 111 | +def _resolve_fuel(name: str) -> str: |
| 112 | + code = FUELS.get(name.lower()) |
| 113 | + if not code: |
| 114 | + raise ValueError(f"Unknown fuel '{name}'. Valid: {', '.join(sorted(FUELS))}") |
| 115 | + return code |
| 116 | + |
| 117 | + |
| 118 | +def _resolve_transmission(name: str) -> str: |
| 119 | + code = TRANSMISSIONS.get(name.lower()) |
| 120 | + if not code: |
| 121 | + raise ValueError(f"Unknown transmission '{name}'. Valid: {', '.join(sorted(TRANSMISSIONS))}") |
| 122 | + return code |
| 123 | + |
| 124 | + |
71 | 125 | def _resolve_location(name: str) -> str: |
72 | 126 | code = LOCATIONS.get(name.lower()) |
73 | 127 | if not code: |
@@ -157,3 +211,114 @@ def handle_endtag(self, tag): |
157 | 211 | continue |
158 | 212 |
|
159 | 213 | return None |
| 214 | + |
| 215 | + |
| 216 | +def search_cars( |
| 217 | + query: str, |
| 218 | + *, |
| 219 | + location: str | None = None, |
| 220 | + price_min: int | None = None, |
| 221 | + price_max: int | None = None, |
| 222 | + year_min: int | None = None, |
| 223 | + year_max: int | None = None, |
| 224 | + mileage_min: int | None = None, |
| 225 | + mileage_max: int | None = None, |
| 226 | + fuel: str | None = None, |
| 227 | + transmission: str | None = None, |
| 228 | + sort: str | None = None, |
| 229 | + page: int = 1, |
| 230 | +) -> dict: |
| 231 | + """Search used cars on Blocket.""" |
| 232 | + params: list[tuple[str, str]] = [("q", query), ("page", str(page))] |
| 233 | + if location: |
| 234 | + params.append(("location", _resolve_location(location))) |
| 235 | + if price_min is not None: |
| 236 | + params.append(("price_from", str(price_min))) |
| 237 | + if price_max is not None: |
| 238 | + params.append(("price_to", str(price_max))) |
| 239 | + if year_min is not None: |
| 240 | + params.append(("year_from", str(year_min))) |
| 241 | + if year_max is not None: |
| 242 | + params.append(("year_to", str(year_max))) |
| 243 | + if mileage_min is not None: |
| 244 | + params.append(("mileage_from", str(mileage_min))) |
| 245 | + if mileage_max is not None: |
| 246 | + params.append(("mileage_to", str(mileage_max))) |
| 247 | + if fuel: |
| 248 | + params.append(("fuel", _resolve_fuel(fuel))) |
| 249 | + if transmission: |
| 250 | + params.append(("transmission", _resolve_transmission(transmission))) |
| 251 | + if sort: |
| 252 | + params.append(("sort", sort)) |
| 253 | + |
| 254 | + r = httpx.get(CAR_SEARCH_URL, params=params, headers=HEADERS, timeout=15, follow_redirects=True) |
| 255 | + r.raise_for_status() |
| 256 | + return r.json() |
| 257 | + |
| 258 | + |
| 259 | +def search_boats( |
| 260 | + query: str, |
| 261 | + *, |
| 262 | + location: str | None = None, |
| 263 | + price_min: int | None = None, |
| 264 | + price_max: int | None = None, |
| 265 | + length_min: int | None = None, |
| 266 | + length_max: int | None = None, |
| 267 | + sort: str | None = None, |
| 268 | + page: int = 1, |
| 269 | +) -> dict: |
| 270 | + """Search used boats on Blocket.""" |
| 271 | + params: list[tuple[str, str]] = [("q", query), ("page", str(page))] |
| 272 | + if location: |
| 273 | + params.append(("location", _resolve_location(location))) |
| 274 | + if price_min is not None: |
| 275 | + params.append(("price_from", str(price_min))) |
| 276 | + if price_max is not None: |
| 277 | + params.append(("price_to", str(price_max))) |
| 278 | + if length_min is not None: |
| 279 | + params.append(("length_feet_from", str(length_min))) |
| 280 | + if length_max is not None: |
| 281 | + params.append(("length_feet_to", str(length_max))) |
| 282 | + if sort: |
| 283 | + params.append(("sort", sort)) |
| 284 | + |
| 285 | + r = httpx.get(BOAT_SEARCH_URL, params=params, headers=HEADERS, timeout=15, follow_redirects=True) |
| 286 | + r.raise_for_status() |
| 287 | + return r.json() |
| 288 | + |
| 289 | + |
| 290 | +def search_mc( |
| 291 | + query: str, |
| 292 | + *, |
| 293 | + location: str | None = None, |
| 294 | + price_min: int | None = None, |
| 295 | + price_max: int | None = None, |
| 296 | + year_min: int | None = None, |
| 297 | + year_max: int | None = None, |
| 298 | + mileage_min: int | None = None, |
| 299 | + mileage_max: int | None = None, |
| 300 | + sort: str | None = None, |
| 301 | + page: int = 1, |
| 302 | +) -> dict: |
| 303 | + """Search used motorcycles on Blocket.""" |
| 304 | + params: list[tuple[str, str]] = [("q", query), ("page", str(page))] |
| 305 | + if location: |
| 306 | + params.append(("location", _resolve_location(location))) |
| 307 | + if price_min is not None: |
| 308 | + params.append(("price_from", str(price_min))) |
| 309 | + if price_max is not None: |
| 310 | + params.append(("price_to", str(price_max))) |
| 311 | + if year_min is not None: |
| 312 | + params.append(("year_from", str(year_min))) |
| 313 | + if year_max is not None: |
| 314 | + params.append(("year_to", str(year_max))) |
| 315 | + if mileage_min is not None: |
| 316 | + params.append(("mileage_from", str(mileage_min))) |
| 317 | + if mileage_max is not None: |
| 318 | + params.append(("mileage_to", str(mileage_max))) |
| 319 | + if sort: |
| 320 | + params.append(("sort", sort)) |
| 321 | + |
| 322 | + r = httpx.get(MC_SEARCH_URL, params=params, headers=HEADERS, timeout=15, follow_redirects=True) |
| 323 | + r.raise_for_status() |
| 324 | + return r.json() |
0 commit comments