scrapebadger-cli 0.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1117 @@
1
+ """Google scraper commands — 19 product APIs / 39 endpoints.
2
+
3
+ Each subcommand maps 1:1 to a ``/v1/google/*`` route. Use ``scrapebadger
4
+ google --help`` to see the full list.
5
+
6
+ Typical workflow:
7
+
8
+ # Web search
9
+ scrapebadger google search "python 3.13"
10
+
11
+ # Shopping search; pipe gpcid into products detail for full specs + offers
12
+ scrapebadger google shopping "laptop"
13
+ scrapebadger google product-detail 7768723536481519578 --q "laptop" --include-offers
14
+ """
15
+
16
+ import typer
17
+
18
+ from scrapebadger_cli.client import api_get
19
+ from scrapebadger_cli.output import render
20
+
21
+ app = typer.Typer(no_args_is_help=True)
22
+
23
+ FMT = typer.Option("json", "--output", "-o", help="Output format: json, csv, table, markdown")
24
+ FIELDS = typer.Option(None, "--fields", "-f", help="Comma-separated fields to include")
25
+
26
+
27
+ # ---------------------------------------------------------------------------
28
+ # Search
29
+ # ---------------------------------------------------------------------------
30
+
31
+
32
+ @app.command()
33
+ def search(
34
+ q: str = typer.Argument(..., help="Search query"),
35
+ gl: str | None = typer.Option(None, "--gl", help="Country code (e.g. us, gb)"),
36
+ hl: str | None = typer.Option(None, "--hl", help="Language code (e.g. en, fr)"),
37
+ num: int | None = typer.Option(None, "--num", "-n", help="Number of results"),
38
+ start: int | None = typer.Option(None, "--start", help="Page offset (0, 10, 20...)"),
39
+ domain: str | None = typer.Option(None, "--domain", help="Google domain (e.g. google.co.uk)"),
40
+ device: str | None = typer.Option(None, "--device", help="'desktop' or 'mobile'"),
41
+ location: str | None = typer.Option(None, "--location", help="City-level geo-targeting"),
42
+ tbs: str | None = typer.Option(None, "--tbs", help="Time filter (e.g. qdr:d)"),
43
+ safe: str | None = typer.Option(None, "--safe", help="Safe search: off, medium, high"),
44
+ ai_overview: bool = typer.Option(
45
+ False,
46
+ "--ai-overview",
47
+ help="Chase deferred AI Overview page_token when the SERP defers it",
48
+ ),
49
+ fmt: str = FMT,
50
+ fields: str | None = FIELDS,
51
+ ) -> None:
52
+ """Google web search (SERP)."""
53
+ data = api_get(
54
+ "/v1/google/search",
55
+ {
56
+ "q": q,
57
+ "gl": gl,
58
+ "hl": hl,
59
+ "num": num,
60
+ "start": start,
61
+ "domain": domain,
62
+ "device": device,
63
+ "location": location,
64
+ "tbs": tbs,
65
+ "safe": safe,
66
+ "ai_overview": ai_overview if ai_overview else None,
67
+ },
68
+ )
69
+ render(data, fmt, fields)
70
+
71
+
72
+ @app.command(name="light-search")
73
+ def light_search(
74
+ q: str = typer.Argument(..., help="Search query"),
75
+ gl: str | None = typer.Option(None, "--gl", help="Country code"),
76
+ hl: str | None = typer.Option(None, "--hl", help="Language code"),
77
+ num: int | None = typer.Option(None, "--num", "-n", help="Results per page (1-100)"),
78
+ start: int | None = typer.Option(None, "--start", help="Pagination offset"),
79
+ domain: str | None = typer.Option(None, "--domain", help="Google domain"),
80
+ location: str | None = typer.Option(None, "--location", help="City-level geo-targeting"),
81
+ safe: str | None = typer.Option(None, "--safe", help="off / active"),
82
+ fmt: str = FMT,
83
+ fields: str | None = FIELDS,
84
+ ) -> None:
85
+ """Google Light Search — organic + related searches only (~40% faster than full SERP)."""
86
+ data = api_get(
87
+ "/v1/google/search",
88
+ {
89
+ "q": q,
90
+ "gl": gl,
91
+ "hl": hl,
92
+ "num": num,
93
+ "start": start,
94
+ "domain": domain,
95
+ "location": location,
96
+ "safe": safe,
97
+ "mode": "fast",
98
+ },
99
+ )
100
+ render(data, fmt, fields)
101
+
102
+
103
+ # ---------------------------------------------------------------------------
104
+ # Maps (5 endpoints)
105
+ # ---------------------------------------------------------------------------
106
+
107
+
108
+ @app.command(name="maps-search")
109
+ def maps_search(
110
+ q: str = typer.Argument(..., help="Place search query"),
111
+ ll: str | None = typer.Option(None, "--ll", help="GPS coords '@lat,lng,zoom'"),
112
+ gl: str | None = typer.Option(None, "--gl", help="Country code (e.g. us, gb)"),
113
+ hl: str | None = typer.Option(None, "--hl", help="Language code (e.g. en, fr)"),
114
+ start: int | None = typer.Option(None, "--start", help="Pagination offset"),
115
+ fmt: str = FMT,
116
+ fields: str | None = FIELDS,
117
+ ) -> None:
118
+ """Maps place search."""
119
+ data = api_get(
120
+ "/v1/google/maps/search",
121
+ {"q": q, "ll": ll, "gl": gl, "hl": hl, "start": start},
122
+ )
123
+ render(data, fmt, fields)
124
+
125
+
126
+ @app.command(name="maps-place")
127
+ def maps_place(
128
+ place_id: str | None = typer.Option(None, "--place-id", help="Google place ID"),
129
+ data_id: str | None = typer.Option(None, "--data-id", help="Google Maps data ID"),
130
+ gl: str | None = typer.Option(None, "--gl", help="Country code"),
131
+ hl: str | None = typer.Option(None, "--hl", help="Language code"),
132
+ fmt: str = FMT,
133
+ fields: str | None = FIELDS,
134
+ ) -> None:
135
+ """Maps place detail. Pass either --place-id or --data-id."""
136
+ if not place_id and not data_id:
137
+ raise typer.BadParameter("Either --place-id or --data-id is required")
138
+ data = api_get(
139
+ "/v1/google/maps/place",
140
+ {"place_id": place_id, "data_id": data_id, "gl": gl, "hl": hl},
141
+ )
142
+ render(data, fmt, fields)
143
+
144
+
145
+ @app.command(name="maps-reviews")
146
+ def maps_reviews(
147
+ data_id: str = typer.Argument(..., help="Google Maps data ID"),
148
+ sort_by: str | None = typer.Option(
149
+ None,
150
+ "--sort-by",
151
+ help="qualityScore, newestFirst, ratingHigh, ratingLow",
152
+ ),
153
+ hl: str | None = typer.Option(None, "--hl", help="Language code"),
154
+ next_page_token: str | None = typer.Option(
155
+ None, "--next-page-token", help="Pagination token from previous response"
156
+ ),
157
+ results: int | None = typer.Option(None, "--results", "-n", help="Reviews per page (1-20)"),
158
+ fmt: str = FMT,
159
+ fields: str | None = FIELDS,
160
+ ) -> None:
161
+ """Maps reviews for a place."""
162
+ data = api_get(
163
+ "/v1/google/maps/reviews",
164
+ {
165
+ "data_id": data_id,
166
+ "sort_by": sort_by,
167
+ "hl": hl,
168
+ "next_page_token": next_page_token,
169
+ "results": results,
170
+ },
171
+ )
172
+ render(data, fmt, fields)
173
+
174
+
175
+ @app.command(name="maps-photos")
176
+ def maps_photos(
177
+ data_id: str = typer.Argument(..., help="Google Maps data ID"),
178
+ hl: str | None = typer.Option(None, "--hl", help="Language code"),
179
+ next_page_token: str | None = typer.Option(None, "--next-page-token", help="Pagination token"),
180
+ fmt: str = FMT,
181
+ fields: str | None = FIELDS,
182
+ ) -> None:
183
+ """Maps photos for a place."""
184
+ data = api_get(
185
+ "/v1/google/maps/photos",
186
+ {"data_id": data_id, "hl": hl, "next_page_token": next_page_token},
187
+ )
188
+ render(data, fmt, fields)
189
+
190
+
191
+ @app.command(name="maps-posts")
192
+ def maps_posts(
193
+ data_id: str = typer.Argument(..., help="Google Maps data ID"),
194
+ next_page_token: str | None = typer.Option(None, "--next-page-token", help="Pagination token"),
195
+ fmt: str = FMT,
196
+ fields: str | None = FIELDS,
197
+ ) -> None:
198
+ """Maps business posts for a place."""
199
+ data = api_get(
200
+ "/v1/google/maps/posts",
201
+ {"data_id": data_id, "next_page_token": next_page_token},
202
+ )
203
+ render(data, fmt, fields)
204
+
205
+
206
+ # ---------------------------------------------------------------------------
207
+ # News (3 endpoints)
208
+ # ---------------------------------------------------------------------------
209
+
210
+
211
+ @app.command()
212
+ def news(
213
+ q: str = typer.Argument(..., help="News search query"),
214
+ hl: str | None = typer.Option(None, "--hl", help="Language code (e.g. en, fr)"),
215
+ gl: str | None = typer.Option(None, "--gl", help="Country code (e.g. US, GB)"),
216
+ max_results: int | None = typer.Option(None, "--max-results", "-n", help="Max articles"),
217
+ fmt: str = FMT,
218
+ fields: str | None = FIELDS,
219
+ ) -> None:
220
+ """News search."""
221
+ data = api_get(
222
+ "/v1/google/news/search",
223
+ {"q": q, "hl": hl, "gl": gl, "max_results": max_results},
224
+ )
225
+ render(data, fmt, fields)
226
+
227
+
228
+ @app.command(name="news-topics")
229
+ def news_topics(
230
+ topic: str = typer.Argument(
231
+ ...,
232
+ help="WORLD, BUSINESS, TECHNOLOGY, ENTERTAINMENT, SPORTS, SCIENCE, HEALTH",
233
+ ),
234
+ hl: str | None = typer.Option(None, "--hl", help="Language code"),
235
+ gl: str | None = typer.Option(None, "--gl", help="Country code"),
236
+ max_results: int | None = typer.Option(None, "--max-results", "-n"),
237
+ fmt: str = FMT,
238
+ fields: str | None = FIELDS,
239
+ ) -> None:
240
+ """News articles for a predefined topic."""
241
+ data = api_get(
242
+ "/v1/google/news/topics",
243
+ {"topic": topic, "hl": hl, "gl": gl, "max_results": max_results},
244
+ )
245
+ render(data, fmt, fields)
246
+
247
+
248
+ @app.command(name="news-trending")
249
+ def news_trending(
250
+ hl: str | None = typer.Option(None, "--hl", help="Language code"),
251
+ gl: str | None = typer.Option(None, "--gl", help="Country code"),
252
+ max_results: int | None = typer.Option(None, "--max-results", "-n"),
253
+ fmt: str = FMT,
254
+ fields: str | None = FIELDS,
255
+ ) -> None:
256
+ """Trending news stories."""
257
+ data = api_get(
258
+ "/v1/google/news/trending",
259
+ {"hl": hl, "gl": gl, "max_results": max_results},
260
+ )
261
+ render(data, fmt, fields)
262
+
263
+
264
+ # ---------------------------------------------------------------------------
265
+ # Hotels (2 endpoints)
266
+ # ---------------------------------------------------------------------------
267
+
268
+
269
+ @app.command()
270
+ def hotels(
271
+ q: str = typer.Argument(..., help="Hotel search query or destination"),
272
+ check_in: str | None = typer.Option(None, "--check-in", help="Check-in date (YYYY-MM-DD)"),
273
+ check_out: str | None = typer.Option(None, "--check-out", help="Check-out date (YYYY-MM-DD)"),
274
+ adults: int | None = typer.Option(None, "--adults", "-a", help="Number of adult guests"),
275
+ currency: str | None = typer.Option(None, "--currency", help="Currency code (e.g. USD, EUR)"),
276
+ gl: str | None = typer.Option(None, "--gl", help="Country code"),
277
+ fmt: str = FMT,
278
+ fields: str | None = FIELDS,
279
+ ) -> None:
280
+ """Hotels search."""
281
+ data = api_get(
282
+ "/v1/google/hotels/search",
283
+ {
284
+ "q": q,
285
+ "check_in": check_in,
286
+ "check_out": check_out,
287
+ "adults": adults,
288
+ "currency": currency,
289
+ "gl": gl,
290
+ },
291
+ )
292
+ render(data, fmt, fields)
293
+
294
+
295
+ @app.command(name="hotels-details")
296
+ def hotels_details(
297
+ property_token: str = typer.Argument(..., help="Property token from search results"),
298
+ check_in: str = typer.Option(..., "--check-in", help="Check-in date (YYYY-MM-DD)"),
299
+ check_out: str = typer.Option(..., "--check-out", help="Check-out date (YYYY-MM-DD)"),
300
+ fmt: str = FMT,
301
+ fields: str | None = FIELDS,
302
+ ) -> None:
303
+ """Hotel property details."""
304
+ data = api_get(
305
+ "/v1/google/hotels/details",
306
+ {"property_token": property_token, "check_in": check_in, "check_out": check_out},
307
+ )
308
+ render(data, fmt, fields)
309
+
310
+
311
+ # ---------------------------------------------------------------------------
312
+ # Trends (4 endpoints)
313
+ # ---------------------------------------------------------------------------
314
+
315
+
316
+ @app.command()
317
+ def trends(
318
+ q: str = typer.Argument(..., help="Up to 5 comma-separated terms"),
319
+ geo: str | None = typer.Option(None, "--geo", help="Location code (e.g. US, GB)"),
320
+ date: str | None = typer.Option(
321
+ None, "--date", help="Time range (e.g. 'now 7-d', 'today 12-m', 'all')"
322
+ ),
323
+ fmt: str = FMT,
324
+ fields: str | None = FIELDS,
325
+ ) -> None:
326
+ """Trends: interest over time."""
327
+ data = api_get("/v1/google/trends/interest", {"q": q, "geo": geo, "date": date})
328
+ render(data, fmt, fields)
329
+
330
+
331
+ @app.command(name="trends-regions")
332
+ def trends_regions(
333
+ q: str = typer.Argument(..., help="Search term"),
334
+ geo: str | None = typer.Option(None, "--geo", help="Geographic scope"),
335
+ fmt: str = FMT,
336
+ fields: str | None = FIELDS,
337
+ ) -> None:
338
+ """Trends: interest by region."""
339
+ data = api_get("/v1/google/trends/regions", {"q": q, "geo": geo})
340
+ render(data, fmt, fields)
341
+
342
+
343
+ @app.command(name="trends-related")
344
+ def trends_related(
345
+ q: str = typer.Argument(..., help="Search term"),
346
+ geo: str | None = typer.Option(None, "--geo", help="Geographic scope"),
347
+ fmt: str = FMT,
348
+ fields: str | None = FIELDS,
349
+ ) -> None:
350
+ """Trends: related topics and queries."""
351
+ data = api_get("/v1/google/trends/related", {"q": q, "geo": geo})
352
+ render(data, fmt, fields)
353
+
354
+
355
+ @app.command(name="trends-trending")
356
+ def trends_trending(
357
+ geo: str | None = typer.Option(None, "--geo", help="Region code (e.g. US)"),
358
+ fmt: str = FMT,
359
+ fields: str | None = FIELDS,
360
+ ) -> None:
361
+ """Trends: real-time trending searches (legacy single-param)."""
362
+ data = api_get("/v1/google/trends/trending", {"geo": geo})
363
+ render(data, fmt, fields)
364
+
365
+
366
+ @app.command(name="trends-trending-now")
367
+ def trends_trending_now(
368
+ geo: str | None = typer.Option(None, "--geo", help="Country code (e.g. US, LT, GB)"),
369
+ hours: int | None = typer.Option(None, "--hours", help="Look-back window: 4, 24, 48, 168"),
370
+ category: str | None = typer.Option(
371
+ None,
372
+ "--category",
373
+ help="all | business | entertainment | health | sci_tech | sports | top_stories",
374
+ ),
375
+ status: str | None = typer.Option(None, "--status", help="all | active"),
376
+ sort: str | None = typer.Option(
377
+ None,
378
+ "--sort",
379
+ help="relevance | search_volume | title | recency",
380
+ ),
381
+ hl: str | None = typer.Option(None, "--hl", help="Language code"),
382
+ fmt: str = FMT,
383
+ fields: str | None = FIELDS,
384
+ ) -> None:
385
+ """Trends: trending searches with the full Google UI filter set."""
386
+ data = api_get(
387
+ "/v1/google/trends/trending-now",
388
+ {
389
+ "geo": geo,
390
+ "hours": hours,
391
+ "category": category,
392
+ "status": status,
393
+ "sort": sort,
394
+ "hl": hl,
395
+ },
396
+ )
397
+ render(data, fmt, fields)
398
+
399
+
400
+ @app.command(name="trends-search")
401
+ def trends_search(
402
+ q: str = typer.Argument(..., help="Up to 5 comma-separated terms (or /m/... MID)"),
403
+ data_type: str = typer.Option(
404
+ "TIMESERIES",
405
+ "--data-type",
406
+ "-d",
407
+ help="TIMESERIES | GEO_MAP | GEO_MAP_0 | RELATED_TOPICS | RELATED_QUERIES",
408
+ ),
409
+ geo: str | None = typer.Option(None, "--geo", help="Country / region code"),
410
+ date: str | None = typer.Option(None, "--date", help="Time range"),
411
+ cat: int | None = typer.Option(None, "--cat", help="Category ID (0 = all)"),
412
+ gprop: str | None = typer.Option(None, "--gprop", help="images/news/froogle/youtube"),
413
+ region: str | None = typer.Option(None, "--region", help="COUNTRY/REGION/DMA/CITY"),
414
+ language: str | None = typer.Option(None, "--language", help="Alias for hl"),
415
+ tz: str | None = typer.Option(None, "--tz", help="Timezone offset (minutes)"),
416
+ fmt: str = FMT,
417
+ fields: str | None = FIELDS,
418
+ ) -> None:
419
+ """Trends: unified search dispatcher (data_type picks the response shape)."""
420
+ data = api_get(
421
+ "/v1/google/trends/search",
422
+ {
423
+ "q": q,
424
+ "data_type": data_type,
425
+ "geo": geo,
426
+ "date": date,
427
+ "cat": cat,
428
+ "gprop": gprop,
429
+ "region": region,
430
+ "language": language,
431
+ "tz": tz,
432
+ },
433
+ )
434
+ render(data, fmt, fields)
435
+
436
+
437
+ @app.command(name="trends-autocomplete")
438
+ def trends_autocomplete(
439
+ q: str = typer.Argument(..., help="Query prefix to resolve into Trends topics"),
440
+ hl: str | None = typer.Option(None, "--hl", help="Language code (e.g. en-US)"),
441
+ tz: str | None = typer.Option(None, "--tz", help="Timezone offset in minutes"),
442
+ fmt: str = FMT,
443
+ fields: str | None = FIELDS,
444
+ ) -> None:
445
+ """Trends: categorized Knowledge Graph topic entities (mid, type)."""
446
+ data = api_get("/v1/google/trends/autocomplete", {"q": q, "hl": hl, "tz": tz})
447
+ render(data, fmt, fields)
448
+
449
+
450
+ # ---------------------------------------------------------------------------
451
+ # Jobs
452
+ # ---------------------------------------------------------------------------
453
+
454
+
455
+ @app.command()
456
+ def jobs(
457
+ q: str = typer.Argument(..., help="Job search query"),
458
+ mode: str = typer.Option(
459
+ "rpc",
460
+ "--mode",
461
+ help="rpc (default, Google Careers, ~1-2s) or serp (public Jobs aggregator)",
462
+ ),
463
+ location: str | None = typer.Option(None, "--location", "-l", help="Location (SERP mode)"),
464
+ gl: str | None = typer.Option(None, "--gl", help="Country code"),
465
+ country: str | None = typer.Option(None, "--country", help="Alias for --gl"),
466
+ hl: str | None = typer.Option(None, "--hl", help="Language code"),
467
+ language: str | None = typer.Option(None, "--language", help="Alias for --hl (e.g. en-US)"),
468
+ domain: str | None = typer.Option(
469
+ None, "--domain", help="Google domain (google.com, google.co.uk, …)"
470
+ ),
471
+ job_type: str | None = typer.Option(
472
+ None, "--job-type", help="FULLTIME / PARTTIME / CONTRACTOR / INTERN (SERP)"
473
+ ),
474
+ date_posted: str | None = typer.Option(
475
+ None, "--date-posted", help="today/3days/week/month (SERP)"
476
+ ),
477
+ ltype: str | None = typer.Option(
478
+ None, "--ltype", help="remote/hybrid/onsite/work_from_home (SERP)"
479
+ ),
480
+ chips: str | None = typer.Option(None, "--chips", help="Raw chip filter string (SERP)"),
481
+ uds: str | None = typer.Option(None, "--uds", help="Opaque filter token (SERP)"),
482
+ uule: str | None = typer.Option(None, "--uule", help="UULE-encoded location (SERP)"),
483
+ lrad: str | None = typer.Option(None, "--lrad", help="Search radius in miles (SERP)"),
484
+ next_page_token: str | None = typer.Option(None, "--next-page-token", help="Pagination cursor"),
485
+ fmt: str = FMT,
486
+ fields: str | None = FIELDS,
487
+ ) -> None:
488
+ """Google Jobs search (default: Google Careers RPC, ~1-2s)."""
489
+ data = api_get(
490
+ "/v1/google/jobs/search",
491
+ {
492
+ "q": q,
493
+ "mode": mode,
494
+ "location": location,
495
+ "gl": gl,
496
+ "country": country,
497
+ "hl": hl,
498
+ "language": language,
499
+ "domain": domain,
500
+ "job_type": job_type,
501
+ "date_posted": date_posted,
502
+ "ltype": ltype,
503
+ "chips": chips,
504
+ "uds": uds,
505
+ "uule": uule,
506
+ "lrad": lrad,
507
+ "next_page_token": next_page_token,
508
+ },
509
+ )
510
+ render(data, fmt, fields)
511
+
512
+
513
+ # ---------------------------------------------------------------------------
514
+ # Shopping (3 endpoints)
515
+ # ---------------------------------------------------------------------------
516
+
517
+
518
+ @app.command()
519
+ def shopping(
520
+ q: str = typer.Argument(..., help="Product search query"),
521
+ gl: str | None = typer.Option(None, "--gl", help="Country code"),
522
+ hl: str | None = typer.Option(None, "--hl", help="Language code"),
523
+ domain: str | None = typer.Option(None, "--domain", help="Google domain"),
524
+ page: int | None = typer.Option(None, "--page", help="Zero-based page (each ≈ 60 tiles)"),
525
+ min_price: float | None = typer.Option(None, "--min-price", help="Minimum price filter"),
526
+ max_price: float | None = typer.Option(None, "--max-price", help="Maximum price filter"),
527
+ sort_by: str | None = typer.Option(
528
+ None, "--sort-by", help="price_low, price_high, rating, reviews"
529
+ ),
530
+ free_shipping: bool = typer.Option(False, "--free-shipping", help="Free-shipping filter"),
531
+ on_sale: bool = typer.Option(False, "--on-sale", help="On-sale filter"),
532
+ safe: str | None = typer.Option(None, "--safe", help="off / active"),
533
+ nfpr: int | None = typer.Option(None, "--nfpr", help="1 disables auto-correction"),
534
+ lr: str | None = typer.Option(None, "--lr", help="Language restrict (lang_en)"),
535
+ tbs: str | None = typer.Option(None, "--tbs", help="Raw tbs filter"),
536
+ shoprs: str | None = typer.Option(None, "--shoprs", help="Google internal helper token"),
537
+ fmt: str = FMT,
538
+ fields: str | None = FIELDS,
539
+ ) -> None:
540
+ """Google Shopping (udm=28) — product tiles with rich fields."""
541
+ data = api_get(
542
+ "/v1/google/shopping/search",
543
+ {
544
+ "q": q,
545
+ "gl": gl,
546
+ "hl": hl,
547
+ "domain": domain,
548
+ "page": page,
549
+ "min_price": min_price,
550
+ "max_price": max_price,
551
+ "sort_by": sort_by,
552
+ "free_shipping": "true" if free_shipping else None,
553
+ "on_sale": "true" if on_sale else None,
554
+ "safe": safe,
555
+ "nfpr": nfpr,
556
+ "lr": lr,
557
+ "tbs": tbs,
558
+ "shoprs": shoprs,
559
+ },
560
+ )
561
+ render(data, fmt, fields)
562
+
563
+
564
+ @app.command()
565
+ def shopping_offers(
566
+ barcode: str = typer.Argument(..., help="Product barcode — GTIN-8 / UPC-A / EAN-13 / GTIN-14"),
567
+ gl: str | None = typer.Option(None, "--gl", help="Country code (ISO 3166 alpha-2)"),
568
+ hl: str | None = typer.Option(None, "--hl", help="Language code"),
569
+ fmt: str = FMT,
570
+ fields: str | None = FIELDS,
571
+ ) -> None:
572
+ """Multi-seller Google Shopping prices for a product by barcode (GTIN/EAN/UPC)."""
573
+ data = api_get(
574
+ "/v1/google/shopping/offers",
575
+ {"barcode": barcode, "gl": gl, "hl": hl},
576
+ )
577
+ render(data, fmt, fields)
578
+
579
+
580
+ # ---------------------------------------------------------------------------
581
+ # Patents (2 endpoints)
582
+ # ---------------------------------------------------------------------------
583
+
584
+
585
+ @app.command()
586
+ def patents(
587
+ q: str = typer.Argument(..., help="Patent search query (Boolean logic supported)"),
588
+ inventor: str | None = typer.Option(None, "--inventor", help="Filter by inventor name"),
589
+ assignee: str | None = typer.Option(None, "--assignee", help="Filter by assignee/company"),
590
+ country: str | None = typer.Option(None, "--country", help="Country code (US, EP, WO, …)"),
591
+ language: str | None = typer.Option(
592
+ None, "--language", help="ENGLISH, GERMAN, CHINESE, FRENCH, JAPANESE, KOREAN, SPANISH"
593
+ ),
594
+ status: str | None = typer.Option(None, "--status", help="GRANT or APPLICATION"),
595
+ patent_type: str | None = typer.Option(None, "--patent-type", help="PATENT or DESIGN"),
596
+ before: str | None = typer.Option(None, "--before", help="Before date YYYYMMDD"),
597
+ after: str | None = typer.Option(None, "--after", help="After date YYYYMMDD"),
598
+ sort: str | None = typer.Option(None, "--sort", help="'new' or 'old'"),
599
+ page: int | None = typer.Option(None, "--page", help="Page number (0-indexed)"),
600
+ num: int | None = typer.Option(None, "--num", "-n", help="Results per page (1-100)"),
601
+ fmt: str = FMT,
602
+ fields: str | None = FIELDS,
603
+ ) -> None:
604
+ """Patents search via the /xhr/query JSON RPC."""
605
+ data = api_get(
606
+ "/v1/google/patents/search",
607
+ {
608
+ "q": q,
609
+ "inventor": inventor,
610
+ "assignee": assignee,
611
+ "country": country,
612
+ "language": language,
613
+ "status": status,
614
+ "patent_type": patent_type,
615
+ "before": before,
616
+ "after": after,
617
+ "sort": sort,
618
+ "page": page,
619
+ "num": num,
620
+ },
621
+ )
622
+ render(data, fmt, fields)
623
+
624
+
625
+ @app.command()
626
+ def patent(
627
+ patent_id: str = typer.Argument(..., help="Publication number (e.g. US10000000B2)"),
628
+ fmt: str = FMT,
629
+ fields: str | None = FIELDS,
630
+ ) -> None:
631
+ """Rich patent detail via the /xhr/result endpoint.
632
+
633
+ Returns full abstract, every claim, complete description,
634
+ structured CPC classifications, split backward/forward citations,
635
+ non-patent citations, concepts, legal events, and figures.
636
+ """
637
+ data = api_get("/v1/google/patents/detail", {"patent_id": patent_id})
638
+ render(data, fmt, fields)
639
+
640
+
641
+ # ---------------------------------------------------------------------------
642
+ # Scholar
643
+ # ---------------------------------------------------------------------------
644
+
645
+
646
+ @app.command()
647
+ def scholar(
648
+ q: str = typer.Argument(..., help="Scholarly search query"),
649
+ hl: str | None = typer.Option(None, "--hl", help="Language code"),
650
+ as_ylo: int | None = typer.Option(None, "--as-ylo", help="Year lower bound"),
651
+ as_yhi: int | None = typer.Option(None, "--as-yhi", help="Year upper bound"),
652
+ as_sdt: str | None = typer.Option(
653
+ None, "--as-sdt", help="'0' excludes patents, '7' includes them"
654
+ ),
655
+ page: int | None = typer.Option(None, "--page", help="Page number (0-based)"),
656
+ num: int | None = typer.Option(None, "--num", "-n", help="Results per page (1-20)"),
657
+ fmt: str = FMT,
658
+ fields: str | None = FIELDS,
659
+ ) -> None:
660
+ """Google Scholar search.
661
+
662
+ Each result carries its doc id + type badge, wrapped inline_links
663
+ (versions + cited_by + related), PDF resources, and structured
664
+ authors (with author_id for profiled authors — pipe into
665
+ `scholar-author`). Envelope includes `scholar_results` alias,
666
+ `related_searches`, and matched `profiles` cards.
667
+ """
668
+ data = api_get(
669
+ "/v1/google/scholar/search",
670
+ {
671
+ "q": q,
672
+ "hl": hl,
673
+ "as_ylo": as_ylo,
674
+ "as_yhi": as_yhi,
675
+ "as_sdt": as_sdt,
676
+ "page": page,
677
+ "num": num,
678
+ },
679
+ )
680
+ render(data, fmt, fields)
681
+
682
+
683
+ @app.command(name="scholar-profiles")
684
+ def scholar_profiles(
685
+ mauthors: str = typer.Argument(..., help="Author name query"),
686
+ hl: str | None = typer.Option(None, "--hl", help="Language code"),
687
+ after_author: str | None = typer.Option(
688
+ None, "--after-author", help="Next-page pagination token"
689
+ ),
690
+ before_author: str | None = typer.Option(
691
+ None, "--before-author", help="Previous-page pagination token"
692
+ ),
693
+ fmt: str = FMT,
694
+ fields: str | None = FIELDS,
695
+ ) -> None:
696
+ """Search Google Scholar for author profiles by name."""
697
+ data = api_get(
698
+ "/v1/google/scholar/profiles",
699
+ {
700
+ "mauthors": mauthors,
701
+ "hl": hl,
702
+ "after_author": after_author,
703
+ "before_author": before_author,
704
+ },
705
+ )
706
+ render(data, fmt, fields)
707
+
708
+
709
+ @app.command(name="scholar-author")
710
+ def scholar_author(
711
+ author_id: str = typer.Argument(..., help="Scholar user ID"),
712
+ hl: str | None = typer.Option(None, "--hl", help="Language code"),
713
+ cstart: int | None = typer.Option(None, "--cstart", help="Articles pagination offset"),
714
+ pagesize: int | None = typer.Option(None, "--pagesize", help="Articles per page"),
715
+ fmt: str = FMT,
716
+ fields: str | None = FIELDS,
717
+ ) -> None:
718
+ """Full Scholar author profile.
719
+
720
+ Returns structured interests ({title, link}), publications with
721
+ per-article citation_id + nested cited_by{value, link, citation_id}
722
+ blocks, lifetime + since-year stats (citations / h-index /
723
+ i10-index), and co-authors with their author_id.
724
+ """
725
+ data = api_get(
726
+ "/v1/google/scholar/author",
727
+ {
728
+ "author_id": author_id,
729
+ "hl": hl,
730
+ "cstart": cstart,
731
+ "pagesize": pagesize,
732
+ },
733
+ )
734
+ render(data, fmt, fields)
735
+
736
+
737
+ @app.command(name="scholar-author-citation")
738
+ def scholar_author_citation(
739
+ author_id: str = typer.Argument(..., help="Scholar user ID"),
740
+ hl: str | None = typer.Option(None, "--hl", help="Language code"),
741
+ fmt: str = FMT,
742
+ fields: str | None = FIELDS,
743
+ ) -> None:
744
+ """Citations-per-year chart for a Scholar author."""
745
+ data = api_get(
746
+ "/v1/google/scholar/author/citation",
747
+ {"author_id": author_id, "hl": hl},
748
+ )
749
+ render(data, fmt, fields)
750
+
751
+
752
+ @app.command(name="scholar-cite")
753
+ def scholar_cite(
754
+ q: str = typer.Argument(..., help="Cluster ID from a search result"),
755
+ hl: str | None = typer.Option(None, "--hl", help="Language code"),
756
+ fmt: str = FMT,
757
+ fields: str | None = FIELDS,
758
+ ) -> None:
759
+ """MLA / APA / Chicago / Harvard / Vancouver citation formats for a paper."""
760
+ data = api_get("/v1/google/scholar/cite", {"q": q, "hl": hl})
761
+ render(data, fmt, fields)
762
+
763
+
764
+ # ---------------------------------------------------------------------------
765
+ # Autocomplete
766
+ # ---------------------------------------------------------------------------
767
+
768
+
769
+ @app.command()
770
+ def autocomplete(
771
+ q: str = typer.Argument(..., help="Query prefix to get suggestions for"),
772
+ hl: str | None = typer.Option(None, "--hl", help="Language code"),
773
+ gl: str | None = typer.Option(None, "--gl", help="Country code"),
774
+ fmt: str = FMT,
775
+ fields: str | None = FIELDS,
776
+ ) -> None:
777
+ """Google search autocomplete suggestions."""
778
+ data = api_get("/v1/google/autocomplete", {"q": q, "hl": hl, "gl": gl})
779
+ render(data, fmt, fields)
780
+
781
+
782
+ # ---------------------------------------------------------------------------
783
+ # Images
784
+ # ---------------------------------------------------------------------------
785
+
786
+
787
+ @app.command()
788
+ def images(
789
+ q: str = typer.Argument(..., help="Image search query"),
790
+ gl: str | None = typer.Option(None, "--gl", help="Country code"),
791
+ hl: str | None = typer.Option(None, "--hl", help="Language code"),
792
+ page: int | None = typer.Option(None, "--page", help="Zero-based page (each page ≈ 100 tiles)"),
793
+ results: int | None = typer.Option(None, "--results", help="Max tiles to return (1-500 cap)"),
794
+ safe: str | None = typer.Option(None, "--safe", help="off, active"),
795
+ tbs: str | None = typer.Option(None, "--tbs", help="Raw tbs filter (e.g. qdr:d)"),
796
+ imgsz: str | None = typer.Option(None, "--imgsz", help="Image size: l, m, i, xXl, xxl, xxxl"),
797
+ imgcolor: str | None = typer.Option(None, "--imgcolor", help="Image color filter"),
798
+ imgtype: str | None = typer.Option(
799
+ None, "--imgtype", help="face, photo, clipart, lineart, animated"
800
+ ),
801
+ fmt: str = FMT,
802
+ fields: str | None = FIELDS,
803
+ ) -> None:
804
+ """Google Images search — title/source/link/thumbnail/original/dimensions/size."""
805
+ data = api_get(
806
+ "/v1/google/images/search",
807
+ {
808
+ "q": q,
809
+ "gl": gl,
810
+ "hl": hl,
811
+ "page": page,
812
+ "results": results,
813
+ "safe": safe,
814
+ "tbs": tbs,
815
+ "imgsz": imgsz,
816
+ "imgcolor": imgcolor,
817
+ "imgtype": imgtype,
818
+ },
819
+ )
820
+ render(data, fmt, fields)
821
+
822
+
823
+ # ---------------------------------------------------------------------------
824
+ # Videos
825
+ # ---------------------------------------------------------------------------
826
+
827
+
828
+ @app.command()
829
+ def videos(
830
+ q: str = typer.Argument(..., help="Video search query"),
831
+ gl: str | None = typer.Option(None, "--gl", help="Country code"),
832
+ hl: str | None = typer.Option(None, "--hl", help="Language code"),
833
+ page: int | None = typer.Option(None, "--page", help="Zero-based page (each page ≈ 10 tiles)"),
834
+ domain: str | None = typer.Option(None, "--domain", help="Google domain"),
835
+ location: str | None = typer.Option(None, "--location", help="City-level geo-targeting"),
836
+ lr: str | None = typer.Option(None, "--lr", help="Language restrict (lang_en)"),
837
+ uule: str | None = typer.Option(None, "--uule", help="UULE-encoded location"),
838
+ nfpr: int | None = typer.Option(None, "--nfpr", help="1 disables auto-correction"),
839
+ safe: str | None = typer.Option(None, "--safe", help="off / active"),
840
+ tbs: str | None = typer.Option(None, "--tbs", help="Time/duration chip (qdr:w, dur:l)"),
841
+ fmt: str = FMT,
842
+ fields: str | None = FIELDS,
843
+ ) -> None:
844
+ """Google Videos search — title/duration/channel/date/video_id per tile."""
845
+ data = api_get(
846
+ "/v1/google/videos/search",
847
+ {
848
+ "q": q,
849
+ "gl": gl,
850
+ "hl": hl,
851
+ "page": page,
852
+ "domain": domain,
853
+ "location": location,
854
+ "lr": lr,
855
+ "uule": uule,
856
+ "nfpr": nfpr,
857
+ "safe": safe,
858
+ "tbs": tbs,
859
+ },
860
+ )
861
+ render(data, fmt, fields)
862
+
863
+
864
+ # ---------------------------------------------------------------------------
865
+ # Finance
866
+ # ---------------------------------------------------------------------------
867
+
868
+
869
+ @app.command()
870
+ def finance(
871
+ q: str = typer.Argument(
872
+ ..., help="Ticker (optional exchange): AAPL, AAPL:NASDAQ, BTC-USD, EURUSD"
873
+ ),
874
+ hl: str | None = typer.Option(None, "--hl", help="Language code"),
875
+ gl: str | None = typer.Option(None, "--gl", help="Country code"),
876
+ fmt: str = FMT,
877
+ fields: str | None = FIELDS,
878
+ ) -> None:
879
+ """Google Finance quote — via mKsvE RPC."""
880
+ data = api_get("/v1/google/finance/quote", {"q": q, "hl": hl, "gl": gl})
881
+ render(data, fmt, fields)
882
+
883
+
884
+ # ---------------------------------------------------------------------------
885
+ # AI Mode
886
+ # ---------------------------------------------------------------------------
887
+
888
+
889
+ @app.command(name="ai-mode")
890
+ def ai_mode(
891
+ q: str = typer.Argument(..., help="Search query for AI-generated answer"),
892
+ gl: str | None = typer.Option(None, "--gl", help="Country code"),
893
+ hl: str | None = typer.Option(None, "--hl", help="Language code"),
894
+ html: bool = typer.Option(
895
+ False,
896
+ "--html/--no-html",
897
+ help="Include the raw answer_html body (large; off by default)",
898
+ ),
899
+ fmt: str = FMT,
900
+ fields: str | None = FIELDS,
901
+ ) -> None:
902
+ """Google AI Mode (generative answer) search.
903
+
904
+ The response carries structured ``text_blocks`` (prose, headings,
905
+ comparison tables, lists), ``references``, a compact ``markdown``
906
+ rendering and — with ``--html`` — the raw ``answer_html`` body.
907
+ Use ``-o markdown`` to print the answer as readable Markdown.
908
+ """
909
+ data = api_get(
910
+ "/v1/google/ai-mode/search",
911
+ {"q": q, "gl": gl, "hl": hl, "include_html": str(html).lower()},
912
+ )
913
+ # For markdown output, prefer the server-rendered answer Markdown over
914
+ # the generic row-table renderer — it's the whole point of the format.
915
+ if fmt == "markdown" and isinstance(data, dict) and data.get("markdown"):
916
+ typer.echo(data["markdown"])
917
+ return
918
+ render(data, fmt, fields)
919
+
920
+
921
+ # ---------------------------------------------------------------------------
922
+ # Lens
923
+ # ---------------------------------------------------------------------------
924
+
925
+
926
+ @app.command()
927
+ def lens(
928
+ url: str = typer.Argument(..., help="Public URL of the image to search visually"),
929
+ query: str | None = typer.Option(None, "--query", help="Text refinement (e.g. 'pizza')"),
930
+ country: str | None = typer.Option(None, "--country", help="ISO country code (alias for --gl)"),
931
+ language: str | None = typer.Option(None, "--language", help="Language code (alias for --hl)"),
932
+ gl: str | None = typer.Option(None, "--gl", help="Country code"),
933
+ hl: str | None = typer.Option(None, "--hl", help="Language code"),
934
+ product: bool = typer.Option(False, "--product", help="Bias towards shoppable matches"),
935
+ visual_matches: bool = typer.Option(
936
+ True, "--visual-matches/--no-visual-matches", help="Include visual-matches carousel"
937
+ ),
938
+ exact_matches: bool = typer.Option(
939
+ False, "--exact-matches", help="Restrict to exact matches only"
940
+ ),
941
+ fmt: str = FMT,
942
+ fields: str | None = FIELDS,
943
+ ) -> None:
944
+ """Google Lens visual image search.
945
+
946
+ Response carries `lens_results` (Scrapingdog parity) with
947
+ source/favicon/thumbnail/tag/in_stock per match, plus
948
+ `related_searches` chips. Legacy `results` alias kept for
949
+ backwards compat.
950
+ """
951
+ data = api_get(
952
+ "/v1/google/lens/search",
953
+ {
954
+ "url": url,
955
+ "query": query,
956
+ "country": country,
957
+ "language": language,
958
+ "gl": gl,
959
+ "hl": hl,
960
+ "product": "true" if product else None,
961
+ "visual_matches": "true" if visual_matches else "false",
962
+ "exact_matches": "true" if exact_matches else None,
963
+ },
964
+ )
965
+ render(data, fmt, fields)
966
+
967
+
968
+ # ---------------------------------------------------------------------------
969
+ # Products (immersive product detail)
970
+ # ---------------------------------------------------------------------------
971
+
972
+
973
+ @app.command(name="product-detail")
974
+ def products_detail(
975
+ product_id: str = typer.Argument(
976
+ ..., help="Google Shopping gpcid (product_id from /shopping/search tiles)"
977
+ ),
978
+ q: str = typer.Option(
979
+ ...,
980
+ "--q",
981
+ "-q",
982
+ help="Original search query that surfaced the product (required)",
983
+ ),
984
+ gl: str | None = typer.Option(None, "--gl", help="Country code"),
985
+ hl: str | None = typer.Option(None, "--hl", help="Language code"),
986
+ catalog_id: str | None = typer.Option(
987
+ None, "--catalog-id", help="Optional catalogid from the Shopping tile"
988
+ ),
989
+ image_docid: str | None = typer.Option(None, "--image-docid", help="Optional imageDocid"),
990
+ headline_offer_docid: str | None = typer.Option(
991
+ None, "--headline-offer-docid", help="Optional headlineOfferDocid"
992
+ ),
993
+ mid: str | None = typer.Option(None, "--mid", help="Optional Knowledge-Graph mid"),
994
+ include_offers: bool = typer.Option(
995
+ False, "--include-offers", help="Also fetch merchant offers (+~1 s)"
996
+ ),
997
+ include_variants: bool = typer.Option(
998
+ False, "--include-variants", help="Also fetch size/colour variants (+~1 s)"
999
+ ),
1000
+ fmt: str = FMT,
1001
+ fields: str | None = FIELDS,
1002
+ ) -> None:
1003
+ """Google immersive product detail (via /async/oapv RPC)."""
1004
+ data = api_get(
1005
+ "/v1/google/products/detail",
1006
+ {
1007
+ "product_id": product_id,
1008
+ "q": q,
1009
+ "gl": gl,
1010
+ "hl": hl,
1011
+ "catalog_id": catalog_id,
1012
+ "image_docid": image_docid,
1013
+ "headline_offer_docid": headline_offer_docid,
1014
+ "mid": mid,
1015
+ "include_offers": "true" if include_offers else None,
1016
+ "include_variants": "true" if include_variants else None,
1017
+ },
1018
+ )
1019
+ render(data, fmt, fields)
1020
+
1021
+
1022
+ # Local Pack is no longer a standalone CLI command — the ``local_results``
1023
+ # array now ships inside ``sb-cli google search`` (main SERP response) with
1024
+ # the same rich per-place fields. For full place_id-keyed metadata use
1025
+ # ``sb-cli google maps-place``.
1026
+
1027
+
1028
+ # ---------------------------------------------------------------------------
1029
+ # Shorts (short-form vertical videos)
1030
+ # ---------------------------------------------------------------------------
1031
+
1032
+
1033
+ @app.command(name="shorts")
1034
+ def shorts_search(
1035
+ q: str = typer.Argument(..., help="Search query"),
1036
+ gl: str | None = typer.Option(None, "--gl", help="Country code"),
1037
+ hl: str | None = typer.Option(None, "--hl", help="Language code"),
1038
+ domain: str | None = typer.Option(None, "--domain", help="Google domain"),
1039
+ num: int | None = typer.Option(None, "--num", "-n", help="Max tiles to return (1-60 cap)"),
1040
+ start: int | None = typer.Option(None, "--start", help="Pagination offset"),
1041
+ safe: str | None = typer.Option(None, "--safe", help="off / active"),
1042
+ nfpr: int | None = typer.Option(None, "--nfpr", help="1 disables auto-correction"),
1043
+ tbs: str | None = typer.Option(None, "--tbs", help="Raw tbs filter (e.g. qdr:d)"),
1044
+ fmt: str = FMT,
1045
+ fields: str | None = FIELDS,
1046
+ ) -> None:
1047
+ """Google Shorts — YouTube Shorts + TikTok + Reels."""
1048
+ data = api_get(
1049
+ "/v1/google/shorts/search",
1050
+ {
1051
+ "q": q,
1052
+ "gl": gl,
1053
+ "hl": hl,
1054
+ "domain": domain,
1055
+ "num": num,
1056
+ "start": start,
1057
+ "safe": safe,
1058
+ "nfpr": nfpr,
1059
+ "tbs": tbs,
1060
+ },
1061
+ )
1062
+ render(data, fmt, fields)
1063
+
1064
+
1065
+ # ---------------------------------------------------------------------------
1066
+ # Flights
1067
+ # ---------------------------------------------------------------------------
1068
+
1069
+
1070
+ @app.command(name="flights")
1071
+ def flights_search(
1072
+ departure_id: str = typer.Argument(..., help="Departure airport IATA (e.g. JFK)"),
1073
+ arrival_id: str = typer.Argument(..., help="Arrival airport IATA (e.g. LHR)"),
1074
+ outbound_date: str = typer.Argument(..., help="Outbound date (YYYY-MM-DD)"),
1075
+ return_date: str | None = typer.Option(
1076
+ None, "--return-date", help="Return date (YYYY-MM-DD, round-trip only)"
1077
+ ),
1078
+ trip_type: str | None = typer.Option(
1079
+ None,
1080
+ "--trip-type",
1081
+ help="round_trip | one_way | multi_city",
1082
+ ),
1083
+ adults: int | None = typer.Option(None, "--adults", help="Adult passengers (1-9)"),
1084
+ children: int | None = typer.Option(None, "--children", help="Children passengers (0-8)"),
1085
+ travel_class: str | None = typer.Option(
1086
+ None,
1087
+ "--travel-class",
1088
+ help="economy | premium_economy | business | first",
1089
+ ),
1090
+ currency: str | None = typer.Option(None, "--currency", help="ISO-4217 currency code"),
1091
+ gl: str | None = typer.Option(None, "--gl", help="Country code"),
1092
+ hl: str | None = typer.Option(None, "--hl", help="Language code"),
1093
+ stops: str | None = typer.Option(None, "--stops", help="any | nonstop | one_stop | two_stops"),
1094
+ max_price: int | None = typer.Option(None, "--max-price", help="Upper price filter"),
1095
+ fmt: str = FMT,
1096
+ fields: str | None = FIELDS,
1097
+ ) -> None:
1098
+ """Google Flights search — one-way, round-trip, or multi-city itineraries."""
1099
+ data = api_get(
1100
+ "/v1/google/flights/search",
1101
+ {
1102
+ "departure_id": departure_id,
1103
+ "arrival_id": arrival_id,
1104
+ "outbound_date": outbound_date,
1105
+ "return_date": return_date,
1106
+ "trip_type": trip_type,
1107
+ "adults": adults,
1108
+ "children": children,
1109
+ "travel_class": travel_class,
1110
+ "currency": currency,
1111
+ "gl": gl,
1112
+ "hl": hl,
1113
+ "stops": stops,
1114
+ "max_price": max_price,
1115
+ },
1116
+ )
1117
+ render(data, fmt, fields)