Skip to content

Commit 1eaaf6b

Browse files
Add vehicle search: cars, boats, and motorcycles
New commands for Blocket's mobility API: - `blocket cars` with year, mileage (mil), fuel, and transmission filters - `blocket boats` with length filter - `blocket mc` with year and mileage filters - `blocket fuels` to list available fuel types Bump version to 0.2.0.
1 parent ee41f07 commit 1eaaf6b

File tree

8 files changed

+852
-4
lines changed

8 files changed

+852
-4
lines changed

blocket_cli/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""Fast CLI for searching Blocket.se — optimized for agents and scripting."""
22

3-
__version__ = "0.1.0"
3+
__version__ = "0.2.0"

blocket_cli/api.py

Lines changed: 166 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@
99

1010
BASE = "https://www.blocket.se"
1111
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"
1215

1316
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)",
1518
}
1619

1720
LOCATIONS = {
@@ -68,6 +71,57 @@
6871
}
6972

7073

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+
71125
def _resolve_location(name: str) -> str:
72126
code = LOCATIONS.get(name.lower())
73127
if not code:
@@ -157,3 +211,114 @@ def handle_endtag(self, tag):
157211
continue
158212

159213
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

Comments
 (0)