scrapebadger-cli 0.14.0__tar.gz → 0.16.0__tar.gz
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.
- {scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/PKG-INFO +14 -1
- {scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/README.md +13 -0
- {scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/pyproject.toml +1 -1
- {scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/depop.py +1 -1
- {scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/google.py +1 -1
- {scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/tiktok.py +264 -13
- {scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/vinted.py +65 -0
- scrapebadger_cli-0.16.0/tests/test_tiktok_pagination.py +36 -0
- scrapebadger_cli-0.16.0/tests/test_vinted_image_search.py +27 -0
- {scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/uv.lock +1 -1
- {scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/.gitignore +0 -0
- {scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/__init__.py +0 -0
- {scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/client.py +0 -0
- {scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/__init__.py +0 -0
- {scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/airbnb.py +0 -0
- {scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/amazon.py +0 -0
- {scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/apartments.py +0 -0
- {scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/app_store.py +0 -0
- {scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/auth.py +0 -0
- {scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/baidu.py +0 -0
- {scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/bing.py +0 -0
- {scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/booking.py +0 -0
- {scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/chatgpt.py +0 -0
- {scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/duckduckgo.py +0 -0
- {scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/ebay.py +0 -0
- {scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/facebook.py +0 -0
- {scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/gemini.py +0 -0
- {scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/google_play.py +0 -0
- {scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/idealista.py +0 -0
- {scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/immobiliare.py +0 -0
- {scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/instagram.py +0 -0
- {scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/leboncoin.py +0 -0
- {scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/linkedin.py +0 -0
- {scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/loopnet.py +0 -0
- {scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/perplexity.py +0 -0
- {scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/realtor.py +0 -0
- {scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/reddit.py +0 -0
- {scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/redfin.py +0 -0
- {scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/stream.py +0 -0
- {scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/twitter.py +0 -0
- {scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/walmart.py +0 -0
- {scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/web.py +0 -0
- {scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/yahoo.py +0 -0
- {scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/yandex.py +0 -0
- {scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/youtube.py +0 -0
- {scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/zillow.py +0 -0
- {scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/config.py +0 -0
- {scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/main.py +0 -0
- {scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/output.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: scrapebadger-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.16.0
|
|
4
4
|
Summary: ScrapeBadger CLI — Twitter, Vinted, Google, and Web Scraping from the command line
|
|
5
5
|
Author: ScrapeBadger Team
|
|
6
6
|
License: MIT
|
|
@@ -58,3 +58,16 @@ scrapebadger vinted search "nike" --sizes 206 --materials 43
|
|
|
58
58
|
The catalog lists 76 public read operations with parameter types and examples.
|
|
59
59
|
Each mobile read costs 1 credit; discovery is free. Sold comparables are a
|
|
60
60
|
Vinted-selected sample, not a complete sales history or guaranteed final sale prices.
|
|
61
|
+
|
|
62
|
+
### Vinted image search
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
scrapebadger vinted search-by-image --image photo.jpg --market nl --price-to 50
|
|
66
|
+
scrapebadger vinted search-by-image --image-url https://example.com/photo.webp --brand-ids 53
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Provide exactly one of `--image`, `--image-url` or `--image-base64`. JPEG, PNG and
|
|
70
|
+
WebP up to 5 MiB are supported. Returns active listings in visual ranking order;
|
|
71
|
+
10 credits per successful request. Filter with `--catalog-ids`, `--brand-ids`,
|
|
72
|
+
`--status-ids`, `--color-ids`, `--size-ids`, `--material-ids` and price bounds.
|
|
73
|
+
Resend the same image and `--time` from pagination when requesting `--page 2`.
|
|
@@ -36,3 +36,16 @@ scrapebadger vinted search "nike" --sizes 206 --materials 43
|
|
|
36
36
|
The catalog lists 76 public read operations with parameter types and examples.
|
|
37
37
|
Each mobile read costs 1 credit; discovery is free. Sold comparables are a
|
|
38
38
|
Vinted-selected sample, not a complete sales history or guaranteed final sale prices.
|
|
39
|
+
|
|
40
|
+
### Vinted image search
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
scrapebadger vinted search-by-image --image photo.jpg --market nl --price-to 50
|
|
44
|
+
scrapebadger vinted search-by-image --image-url https://example.com/photo.webp --brand-ids 53
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Provide exactly one of `--image`, `--image-url` or `--image-base64`. JPEG, PNG and
|
|
48
|
+
WebP up to 5 MiB are supported. Returns active listings in visual ranking order;
|
|
49
|
+
10 credits per successful request. Filter with `--catalog-ids`, `--brand-ids`,
|
|
50
|
+
`--status-ids`, `--color-ids`, `--size-ids`, `--material-ids` and price bounds.
|
|
51
|
+
Resend the same image and `--time` from pagination when requesting `--page 2`.
|
|
@@ -881,7 +881,7 @@ def finance(
|
|
|
881
881
|
fmt: str = FMT,
|
|
882
882
|
fields: str | None = FIELDS,
|
|
883
883
|
) -> None:
|
|
884
|
-
"""Google Finance quote
|
|
884
|
+
"""Google Finance quote."""
|
|
885
885
|
data = api_get("/v1/google/finance/quote", {"q": q, "hl": hl, "gl": gl})
|
|
886
886
|
render(data, fmt, fields)
|
|
887
887
|
|
|
@@ -31,10 +31,16 @@ def user_videos(
|
|
|
31
31
|
count: int = COUNT,
|
|
32
32
|
fmt: str = FMT,
|
|
33
33
|
fields: str | None = FIELDS,
|
|
34
|
+
cursor: str | None = typer.Option(
|
|
35
|
+
None, "--cursor", help="Continuation cursor from the previous page"
|
|
36
|
+
),
|
|
34
37
|
) -> None:
|
|
35
38
|
"""Get a user's posted videos."""
|
|
36
39
|
render(
|
|
37
|
-
api_get(
|
|
40
|
+
api_get(
|
|
41
|
+
f"/v1/tiktok/users/{username}/videos",
|
|
42
|
+
{**({"cursor": cursor} if cursor else {}), "region": region, "count": count},
|
|
43
|
+
),
|
|
38
44
|
fmt,
|
|
39
45
|
fields,
|
|
40
46
|
)
|
|
@@ -47,10 +53,16 @@ def followers(
|
|
|
47
53
|
count: int = COUNT,
|
|
48
54
|
fmt: str = FMT,
|
|
49
55
|
fields: str | None = FIELDS,
|
|
56
|
+
cursor: str | None = typer.Option(
|
|
57
|
+
None, "--cursor", help="Continuation cursor from the previous page"
|
|
58
|
+
),
|
|
50
59
|
) -> None:
|
|
51
60
|
"""Get a user's followers (best-effort)."""
|
|
52
61
|
render(
|
|
53
|
-
api_get(
|
|
62
|
+
api_get(
|
|
63
|
+
f"/v1/tiktok/users/{username}/followers",
|
|
64
|
+
{**({"cursor": cursor} if cursor else {}), "region": region, "count": count},
|
|
65
|
+
),
|
|
54
66
|
fmt,
|
|
55
67
|
fields,
|
|
56
68
|
)
|
|
@@ -63,10 +75,16 @@ def following(
|
|
|
63
75
|
count: int = COUNT,
|
|
64
76
|
fmt: str = FMT,
|
|
65
77
|
fields: str | None = FIELDS,
|
|
78
|
+
cursor: str | None = typer.Option(
|
|
79
|
+
None, "--cursor", help="Continuation cursor from the previous page"
|
|
80
|
+
),
|
|
66
81
|
) -> None:
|
|
67
82
|
"""Get accounts a user follows (best-effort)."""
|
|
68
83
|
render(
|
|
69
|
-
api_get(
|
|
84
|
+
api_get(
|
|
85
|
+
f"/v1/tiktok/users/{username}/following",
|
|
86
|
+
{**({"cursor": cursor} if cursor else {}), "region": region, "count": count},
|
|
87
|
+
),
|
|
70
88
|
fmt,
|
|
71
89
|
fields,
|
|
72
90
|
)
|
|
@@ -98,12 +116,20 @@ def comments(
|
|
|
98
116
|
username: str | None = typer.Option(None, "--username", "-u"),
|
|
99
117
|
fmt: str = FMT,
|
|
100
118
|
fields: str | None = FIELDS,
|
|
119
|
+
cursor: str | None = typer.Option(
|
|
120
|
+
None, "--cursor", help="Continuation cursor from the previous page"
|
|
121
|
+
),
|
|
101
122
|
) -> None:
|
|
102
123
|
"""Get top-level comments on a video."""
|
|
103
124
|
render(
|
|
104
125
|
api_get(
|
|
105
126
|
f"/v1/tiktok/videos/{video_id}/comments",
|
|
106
|
-
{
|
|
127
|
+
{
|
|
128
|
+
**({"cursor": cursor} if cursor else {}),
|
|
129
|
+
"region": region,
|
|
130
|
+
"count": count,
|
|
131
|
+
"username": username,
|
|
132
|
+
},
|
|
107
133
|
),
|
|
108
134
|
fmt,
|
|
109
135
|
fields,
|
|
@@ -118,12 +144,20 @@ def related(
|
|
|
118
144
|
username: str | None = typer.Option(None, "--username", "-u"),
|
|
119
145
|
fmt: str = FMT,
|
|
120
146
|
fields: str | None = FIELDS,
|
|
147
|
+
cursor: str | None = typer.Option(
|
|
148
|
+
None, "--cursor", help="Continuation cursor from the previous page"
|
|
149
|
+
),
|
|
121
150
|
) -> None:
|
|
122
151
|
"""Get related videos for a video."""
|
|
123
152
|
render(
|
|
124
153
|
api_get(
|
|
125
154
|
f"/v1/tiktok/videos/{video_id}/related",
|
|
126
|
-
{
|
|
155
|
+
{
|
|
156
|
+
**({"cursor": cursor} if cursor else {}),
|
|
157
|
+
"region": region,
|
|
158
|
+
"count": count,
|
|
159
|
+
"username": username,
|
|
160
|
+
},
|
|
127
161
|
),
|
|
128
162
|
fmt,
|
|
129
163
|
fields,
|
|
@@ -177,10 +211,16 @@ def hashtag_videos(
|
|
|
177
211
|
count: int = COUNT,
|
|
178
212
|
fmt: str = FMT,
|
|
179
213
|
fields: str | None = FIELDS,
|
|
214
|
+
cursor: str | None = typer.Option(
|
|
215
|
+
None, "--cursor", help="Continuation cursor from the previous page"
|
|
216
|
+
),
|
|
180
217
|
) -> None:
|
|
181
218
|
"""Get videos tagged with a hashtag."""
|
|
182
219
|
render(
|
|
183
|
-
api_get(
|
|
220
|
+
api_get(
|
|
221
|
+
f"/v1/tiktok/hashtags/{name}/videos",
|
|
222
|
+
{**({"cursor": cursor} if cursor else {}), "region": region, "count": count},
|
|
223
|
+
),
|
|
184
224
|
fmt,
|
|
185
225
|
fields,
|
|
186
226
|
)
|
|
@@ -204,10 +244,16 @@ def music_videos(
|
|
|
204
244
|
count: int = COUNT,
|
|
205
245
|
fmt: str = FMT,
|
|
206
246
|
fields: str | None = FIELDS,
|
|
247
|
+
cursor: str | None = typer.Option(
|
|
248
|
+
None, "--cursor", help="Continuation cursor from the previous page"
|
|
249
|
+
),
|
|
207
250
|
) -> None:
|
|
208
251
|
"""Get videos using a sound."""
|
|
209
252
|
render(
|
|
210
|
-
api_get(
|
|
253
|
+
api_get(
|
|
254
|
+
f"/v1/tiktok/music/{music_id}/videos",
|
|
255
|
+
{**({"cursor": cursor} if cursor else {}), "region": region, "count": count},
|
|
256
|
+
),
|
|
211
257
|
fmt,
|
|
212
258
|
fields,
|
|
213
259
|
)
|
|
@@ -221,24 +267,41 @@ def search(
|
|
|
221
267
|
count: int = typer.Option(20, "--count", "-c"),
|
|
222
268
|
fmt: str = FMT,
|
|
223
269
|
fields: str | None = FIELDS,
|
|
270
|
+
cursor: str | None = typer.Option(
|
|
271
|
+
None, "--cursor", help="Continuation cursor from the previous page"
|
|
272
|
+
),
|
|
224
273
|
) -> None:
|
|
225
274
|
"""Search TikTok (general/users/videos/hashtags)."""
|
|
226
275
|
path = "/v1/tiktok/search" if kind == "general" else f"/v1/tiktok/search/{kind}"
|
|
227
|
-
render(
|
|
276
|
+
render(
|
|
277
|
+
api_get(
|
|
278
|
+
path,
|
|
279
|
+
{
|
|
280
|
+
"query": query,
|
|
281
|
+
"region": region,
|
|
282
|
+
"count": count,
|
|
283
|
+
**({"cursor": cursor} if cursor else {}),
|
|
284
|
+
},
|
|
285
|
+
),
|
|
286
|
+
fmt,
|
|
287
|
+
fields,
|
|
288
|
+
)
|
|
228
289
|
|
|
229
290
|
|
|
230
291
|
@app.command()
|
|
231
292
|
def trending(
|
|
232
293
|
kind: str = typer.Option("videos", "--kind", "-k", help="videos | hashtags | songs"),
|
|
233
294
|
region: str = REGION,
|
|
234
|
-
period: int = typer.Option(
|
|
295
|
+
period: int | None = typer.Option(
|
|
296
|
+
None, "--period", help="Historical windows unavailable; omit"
|
|
297
|
+
),
|
|
235
298
|
count: int = typer.Option(20, "--count", "-c"),
|
|
236
299
|
fmt: str = FMT,
|
|
237
300
|
fields: str | None = FIELDS,
|
|
238
301
|
) -> None:
|
|
239
302
|
"""Get trending data (videos/hashtags/songs)."""
|
|
240
303
|
params = {"region": region, "count": count}
|
|
241
|
-
if kind in ("hashtags", "songs"):
|
|
304
|
+
if kind in ("hashtags", "songs") and period is not None:
|
|
242
305
|
params["period"] = period
|
|
243
306
|
render(api_get(f"/v1/tiktok/trending/{kind}", params), fmt, fields)
|
|
244
307
|
|
|
@@ -305,7 +368,12 @@ def ad(
|
|
|
305
368
|
render(api_get(f"/v1/tiktok/ads/{ad_id}", {"region": region}), fmt, fields)
|
|
306
369
|
|
|
307
370
|
|
|
308
|
-
SHOP_REGION = typer.Option(
|
|
371
|
+
SHOP_REGION = typer.Option(
|
|
372
|
+
"US",
|
|
373
|
+
"--region",
|
|
374
|
+
"-r",
|
|
375
|
+
help="Market: US, GB, ID, JP, MX, SG, MY, PH, TH, VN (coverage varies by endpoint)",
|
|
376
|
+
)
|
|
309
377
|
|
|
310
378
|
|
|
311
379
|
@app.command(name="shop-categories")
|
|
@@ -324,17 +392,103 @@ def shop_categories(
|
|
|
324
392
|
render(api_get(path, {"region": region}), fmt, fields)
|
|
325
393
|
|
|
326
394
|
|
|
395
|
+
@app.command(name="shop-category-products")
|
|
396
|
+
def shop_category_products(
|
|
397
|
+
category_id: str = typer.Argument(..., help="Numeric category id"),
|
|
398
|
+
region: str = SHOP_REGION,
|
|
399
|
+
count: int = typer.Option(20, "--count", "-c", min=1, max=40),
|
|
400
|
+
exclude_product_ids: list[str] = typer.Option(
|
|
401
|
+
[], "--exclude-product-id", help="Repeat for every next_exclude_product_ids value"
|
|
402
|
+
),
|
|
403
|
+
fmt: str = FMT,
|
|
404
|
+
fields: str | None = FIELDS,
|
|
405
|
+
) -> None:
|
|
406
|
+
"""Page through a category's product recommendations."""
|
|
407
|
+
render(
|
|
408
|
+
api_get(
|
|
409
|
+
f"/v1/tiktok/shop/categories/{category_id}/products",
|
|
410
|
+
{
|
|
411
|
+
"region": region,
|
|
412
|
+
"count": count,
|
|
413
|
+
"exclude_product_ids": exclude_product_ids,
|
|
414
|
+
},
|
|
415
|
+
),
|
|
416
|
+
fmt,
|
|
417
|
+
fields,
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
@app.command(name="shop-ranking")
|
|
422
|
+
def shop_ranking(
|
|
423
|
+
rank_id: str = typer.Argument(..., help="Native category/theme ranking id"),
|
|
424
|
+
region: str = SHOP_REGION,
|
|
425
|
+
rank_type: int = typer.Option(1, "--rank-type", min=1, max=3),
|
|
426
|
+
cursor: int = typer.Option(0, "--cursor", min=0),
|
|
427
|
+
count: int = typer.Option(20, "--count", "-c", min=1, max=20),
|
|
428
|
+
fmt: str = FMT,
|
|
429
|
+
fields: str | None = FIELDS,
|
|
430
|
+
) -> None:
|
|
431
|
+
"""Native Shop ranking; ID/PH/TH/VN require regional guest appliance profiles."""
|
|
432
|
+
render(
|
|
433
|
+
api_get(
|
|
434
|
+
f"/v1/tiktok/shop/rankings/{rank_id}",
|
|
435
|
+
{
|
|
436
|
+
"region": region,
|
|
437
|
+
"rank_type": rank_type,
|
|
438
|
+
"cursor": cursor,
|
|
439
|
+
"count": count,
|
|
440
|
+
},
|
|
441
|
+
),
|
|
442
|
+
fmt,
|
|
443
|
+
fields,
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
@app.command(name="shop-bestsellers")
|
|
448
|
+
def shop_bestsellers(
|
|
449
|
+
region: str = SHOP_REGION,
|
|
450
|
+
category_id: str | None = typer.Option(None, "--category-id", help="Limit to one category"),
|
|
451
|
+
pages: int = typer.Option(2, "--pages", min=1, max=4),
|
|
452
|
+
limit: int = typer.Option(20, "--limit", "-l", min=1, max=100),
|
|
453
|
+
fmt: str = FMT,
|
|
454
|
+
fields: str | None = FIELDS,
|
|
455
|
+
) -> None:
|
|
456
|
+
"""Sales-ranked best-selling products; works for every market (incl. SG/MY/JP)."""
|
|
457
|
+
render(
|
|
458
|
+
api_get(
|
|
459
|
+
"/v1/tiktok/shop/bestsellers",
|
|
460
|
+
{
|
|
461
|
+
"region": region,
|
|
462
|
+
"category_id": category_id,
|
|
463
|
+
"pages": pages,
|
|
464
|
+
"limit": limit,
|
|
465
|
+
},
|
|
466
|
+
),
|
|
467
|
+
fmt,
|
|
468
|
+
fields,
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
|
|
327
472
|
@app.command(name="shop-search")
|
|
328
473
|
def shop_search(
|
|
329
474
|
query: str = typer.Argument(..., help="Keyword, e.g. 'wireless earbuds'"),
|
|
330
475
|
region: str = SHOP_REGION,
|
|
476
|
+
page_token: str | None = typer.Option(None, "--page-token", help="Native ID/JP continuation"),
|
|
331
477
|
offset: int = typer.Option(0, "--offset", help="Pass back pagination.next_offset (US)"),
|
|
332
478
|
fmt: str = FMT,
|
|
333
479
|
fields: str | None = FIELDS,
|
|
334
480
|
) -> None:
|
|
335
481
|
"""Search TikTok Shop products by keyword (30 per page, offset pagination in the US)."""
|
|
336
482
|
render(
|
|
337
|
-
api_get(
|
|
483
|
+
api_get(
|
|
484
|
+
"/v1/tiktok/shop/search",
|
|
485
|
+
{
|
|
486
|
+
"q": query,
|
|
487
|
+
"region": region,
|
|
488
|
+
"offset": offset,
|
|
489
|
+
"page_token": page_token,
|
|
490
|
+
},
|
|
491
|
+
),
|
|
338
492
|
fmt,
|
|
339
493
|
fields,
|
|
340
494
|
)
|
|
@@ -384,7 +538,7 @@ def shop_reviews(
|
|
|
384
538
|
fmt: str = FMT,
|
|
385
539
|
fields: str | None = FIELDS,
|
|
386
540
|
) -> None:
|
|
387
|
-
"""Paginated
|
|
541
|
+
"""Paginated product reviews (US; Tokopedia source for ID)."""
|
|
388
542
|
params: dict = {
|
|
389
543
|
"region": region,
|
|
390
544
|
"page": page,
|
|
@@ -405,3 +559,100 @@ def regions(
|
|
|
405
559
|
) -> None:
|
|
406
560
|
"""List supported TikTok content regions."""
|
|
407
561
|
render(api_get("/v1/tiktok/regions"), fmt, fields)
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
@app.command(name="shop-mall")
|
|
565
|
+
def shop_mall(
|
|
566
|
+
region: str = SHOP_REGION,
|
|
567
|
+
tab_id: int = typer.Option(0, "--tab-id", min=0),
|
|
568
|
+
page_token: str | None = typer.Option(None, "--page-token"),
|
|
569
|
+
fmt: str = FMT,
|
|
570
|
+
fields: str | None = FIELDS,
|
|
571
|
+
) -> None:
|
|
572
|
+
"""Regional mall navigation and feed; pass back the 15-minute page token."""
|
|
573
|
+
render(
|
|
574
|
+
api_get(
|
|
575
|
+
"/v1/tiktok/shop/mall",
|
|
576
|
+
{
|
|
577
|
+
"region": region,
|
|
578
|
+
"tab_id": tab_id,
|
|
579
|
+
"page_token": page_token,
|
|
580
|
+
},
|
|
581
|
+
),
|
|
582
|
+
fmt,
|
|
583
|
+
fields,
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
@app.command()
|
|
588
|
+
def liked(
|
|
589
|
+
username: str = typer.Argument(..., help="TikTok handle"),
|
|
590
|
+
region: str = REGION,
|
|
591
|
+
count: int = COUNT,
|
|
592
|
+
fmt: str = FMT,
|
|
593
|
+
fields: str | None = FIELDS,
|
|
594
|
+
cursor: str | None = typer.Option(
|
|
595
|
+
None, "--cursor", help="Continuation cursor from the previous page"
|
|
596
|
+
),
|
|
597
|
+
) -> None:
|
|
598
|
+
"""Get accounts a user follows (best-effort)."""
|
|
599
|
+
render(
|
|
600
|
+
api_get(
|
|
601
|
+
f"/v1/tiktok/users/{username}/liked",
|
|
602
|
+
{**({"cursor": cursor} if cursor else {}), "region": region, "count": count},
|
|
603
|
+
),
|
|
604
|
+
fmt,
|
|
605
|
+
fields,
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
@app.command()
|
|
610
|
+
def reposts(
|
|
611
|
+
username: str = typer.Argument(..., help="TikTok handle"),
|
|
612
|
+
region: str = REGION,
|
|
613
|
+
count: int = COUNT,
|
|
614
|
+
fmt: str = FMT,
|
|
615
|
+
fields: str | None = FIELDS,
|
|
616
|
+
cursor: str | None = typer.Option(
|
|
617
|
+
None, "--cursor", help="Continuation cursor from the previous page"
|
|
618
|
+
),
|
|
619
|
+
) -> None:
|
|
620
|
+
"""Get accounts a user follows (best-effort)."""
|
|
621
|
+
render(
|
|
622
|
+
api_get(
|
|
623
|
+
f"/v1/tiktok/users/{username}/reposts",
|
|
624
|
+
{**({"cursor": cursor} if cursor else {}), "region": region, "count": count},
|
|
625
|
+
),
|
|
626
|
+
fmt,
|
|
627
|
+
fields,
|
|
628
|
+
)
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
@app.command()
|
|
632
|
+
def replies(
|
|
633
|
+
comment_id: str = typer.Argument(..., help="Comment id"),
|
|
634
|
+
video_id: str = typer.Argument(..., help="Numeric video id"),
|
|
635
|
+
region: str = REGION,
|
|
636
|
+
count: int = typer.Option(20, "--count", "-c"),
|
|
637
|
+
username: str | None = typer.Option(None, "--username", "-u"),
|
|
638
|
+
fmt: str = FMT,
|
|
639
|
+
fields: str | None = FIELDS,
|
|
640
|
+
cursor: str | None = typer.Option(
|
|
641
|
+
None, "--cursor", help="Continuation cursor from the previous page"
|
|
642
|
+
),
|
|
643
|
+
) -> None:
|
|
644
|
+
"""Get replies to a public comment."""
|
|
645
|
+
render(
|
|
646
|
+
api_get(
|
|
647
|
+
f"/v1/tiktok/comments/{comment_id}/replies",
|
|
648
|
+
{
|
|
649
|
+
**({"cursor": cursor} if cursor else {}),
|
|
650
|
+
"region": region,
|
|
651
|
+
"video_id": video_id,
|
|
652
|
+
"count": count,
|
|
653
|
+
"username": username,
|
|
654
|
+
},
|
|
655
|
+
),
|
|
656
|
+
fmt,
|
|
657
|
+
fields,
|
|
658
|
+
)
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"""Vinted marketplace commands."""
|
|
2
2
|
|
|
3
|
+
import base64
|
|
3
4
|
import json
|
|
4
5
|
import re
|
|
6
|
+
from pathlib import Path
|
|
5
7
|
|
|
6
8
|
import typer
|
|
7
9
|
|
|
@@ -157,3 +159,66 @@ def mobile_read(
|
|
|
157
159
|
fmt,
|
|
158
160
|
fields,
|
|
159
161
|
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@app.command(name="search-by-image")
|
|
165
|
+
def search_by_image(
|
|
166
|
+
image: Path | None = typer.Option(
|
|
167
|
+
None,
|
|
168
|
+
"--image",
|
|
169
|
+
"-i",
|
|
170
|
+
exists=True,
|
|
171
|
+
dir_okay=False,
|
|
172
|
+
help="Local JPEG, PNG or WebP, at most 5 MiB",
|
|
173
|
+
),
|
|
174
|
+
image_url: str | None = typer.Option(None, "--image-url", help="Public image URL"),
|
|
175
|
+
image_base64: str | None = typer.Option(
|
|
176
|
+
None, "--image-base64", help="Base64 image or data URL"
|
|
177
|
+
),
|
|
178
|
+
market: str = MARKET,
|
|
179
|
+
page: int = typer.Option(1, "--page"),
|
|
180
|
+
per_page: int = typer.Option(20, "--per-page"),
|
|
181
|
+
price_from: float | None = typer.Option(None, "--price-from"),
|
|
182
|
+
price_to: float | None = typer.Option(None, "--price-to"),
|
|
183
|
+
brand_ids: str | None = typer.Option(None, "--brand-ids"),
|
|
184
|
+
catalog_ids: str | None = typer.Option(None, "--catalog-ids"),
|
|
185
|
+
color_ids: str | None = typer.Option(None, "--color-ids"),
|
|
186
|
+
size_ids: str | None = typer.Option(None, "--size-ids"),
|
|
187
|
+
material_ids: str | None = typer.Option(None, "--material-ids"),
|
|
188
|
+
status_ids: str | None = typer.Option(None, "--status-ids"),
|
|
189
|
+
time: int | None = typer.Option(None, "--time"),
|
|
190
|
+
search_session_id: str | None = typer.Option(None, "--search-session-id"),
|
|
191
|
+
fmt: str = FMT,
|
|
192
|
+
fields: str | None = FIELDS,
|
|
193
|
+
) -> None:
|
|
194
|
+
"""Find active Vinted listings from a photo. 10 credits per successful search."""
|
|
195
|
+
if sum(bool(x) for x in (image, image_url, image_base64)) != 1:
|
|
196
|
+
raise typer.BadParameter("Pass exactly one of --image, --image-url or --image-base64")
|
|
197
|
+
if image:
|
|
198
|
+
with image.open("rb") as file:
|
|
199
|
+
raw = file.read(5 * 1024 * 1024 + 1)
|
|
200
|
+
if not raw or len(raw) > 5 * 1024 * 1024:
|
|
201
|
+
raise typer.BadParameter("Image must be nonempty and at most 5 MiB")
|
|
202
|
+
image_base64 = base64.b64encode(raw).decode("ascii")
|
|
203
|
+
body = {
|
|
204
|
+
"image_url": image_url,
|
|
205
|
+
"image_base64": image_base64,
|
|
206
|
+
"market": market,
|
|
207
|
+
"page": page,
|
|
208
|
+
"per_page": per_page,
|
|
209
|
+
"price_from": price_from,
|
|
210
|
+
"price_to": price_to,
|
|
211
|
+
"brand_ids": brand_ids,
|
|
212
|
+
"catalog_ids": catalog_ids,
|
|
213
|
+
"color_ids": color_ids,
|
|
214
|
+
"size_ids": size_ids,
|
|
215
|
+
"material_ids": material_ids,
|
|
216
|
+
"status_ids": status_ids,
|
|
217
|
+
"time": time,
|
|
218
|
+
"search_session_id": search_session_id,
|
|
219
|
+
}
|
|
220
|
+
render(
|
|
221
|
+
api_post("/v1/vinted/search_by_image", {k: v for k, v in body.items() if v is not None}),
|
|
222
|
+
fmt,
|
|
223
|
+
fields,
|
|
224
|
+
)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from unittest.mock import patch
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
CliRunner = pytest.importorskip("typer.testing").CliRunner
|
|
6
|
+
app = pytest.importorskip(
|
|
7
|
+
"scrapebadger_cli.commands.tiktok", reason="CLI is tested with its own installed package"
|
|
8
|
+
).app
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.mark.parametrize(
|
|
12
|
+
"args,path",
|
|
13
|
+
[
|
|
14
|
+
(["followers", "tiktok"], "/users/tiktok/followers"),
|
|
15
|
+
(["following", "tiktok"], "/users/tiktok/following"),
|
|
16
|
+
(["liked", "tiktok"], "/users/tiktok/liked"),
|
|
17
|
+
(["reposts", "tiktok"], "/users/tiktok/reposts"),
|
|
18
|
+
(["replies", "123", "456"], "/comments/123/replies"),
|
|
19
|
+
(["search", "news", "--kind", "users"], "/search/users"),
|
|
20
|
+
],
|
|
21
|
+
)
|
|
22
|
+
def test_continuations_are_forwarded(args, path):
|
|
23
|
+
with patch("scrapebadger_cli.commands.tiktok.api_get", return_value={}) as call:
|
|
24
|
+
result = CliRunner().invoke(app, args + ["--cursor", "tw1_test"])
|
|
25
|
+
assert result.exit_code == 0, result.output
|
|
26
|
+
assert call.call_args.args[0] == "/v1/tiktok" + path
|
|
27
|
+
assert call.call_args.args[1]["cursor"] == "tw1_test"
|
|
28
|
+
if args[0] == "replies":
|
|
29
|
+
assert call.call_args.args[1]["video_id"] == "456"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_trends_do_not_invent_a_historical_window():
|
|
33
|
+
with patch("scrapebadger_cli.commands.tiktok.api_get", return_value={}) as call:
|
|
34
|
+
result = CliRunner().invoke(app, ["trending", "--kind", "songs"])
|
|
35
|
+
assert result.exit_code == 0, result.output
|
|
36
|
+
assert "period" not in call.call_args.args[1]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
from unittest.mock import patch
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
CliRunner = pytest.importorskip("typer.testing").CliRunner
|
|
7
|
+
app = pytest.importorskip("scrapebadger_cli.commands.vinted").app
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_local_image_is_encoded_and_filters_forwarded(tmp_path):
|
|
11
|
+
image = tmp_path / "shoe.jpg"
|
|
12
|
+
image.write_bytes(b"\xff\xd8\xfftest")
|
|
13
|
+
with patch("scrapebadger_cli.commands.vinted.api_post", return_value={"items": []}) as call:
|
|
14
|
+
result = CliRunner().invoke(
|
|
15
|
+
app, ["search-by-image", "--image", str(image), "--market", "nl", "--brand-ids", "53"]
|
|
16
|
+
)
|
|
17
|
+
assert result.exit_code == 0, result.output
|
|
18
|
+
path, body = call.call_args.args
|
|
19
|
+
assert path == "/v1/vinted/search_by_image"
|
|
20
|
+
assert base64.b64decode(body["image_base64"]) == image.read_bytes()
|
|
21
|
+
assert body["market"] == "nl" and body["brand_ids"] == "53"
|
|
22
|
+
result = CliRunner().invoke(
|
|
23
|
+
app,
|
|
24
|
+
["search-by-image", "--image", str(image), "--image-url", "https://example.com/a.jpg"],
|
|
25
|
+
)
|
|
26
|
+
assert result.exit_code != 0
|
|
27
|
+
assert call.call_count == 1
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/apartments.py
RENAMED
|
File without changes
|
{scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/app_store.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/booking.py
RENAMED
|
File without changes
|
{scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/chatgpt.py
RENAMED
|
File without changes
|
{scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/duckduckgo.py
RENAMED
|
File without changes
|
|
File without changes
|
{scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/facebook.py
RENAMED
|
File without changes
|
|
File without changes
|
{scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/google_play.py
RENAMED
|
File without changes
|
{scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/idealista.py
RENAMED
|
File without changes
|
{scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/immobiliare.py
RENAMED
|
File without changes
|
{scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/instagram.py
RENAMED
|
File without changes
|
{scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/leboncoin.py
RENAMED
|
File without changes
|
{scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/linkedin.py
RENAMED
|
File without changes
|
{scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/loopnet.py
RENAMED
|
File without changes
|
{scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/perplexity.py
RENAMED
|
File without changes
|
{scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/realtor.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/twitter.py
RENAMED
|
File without changes
|
{scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/walmart.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{scrapebadger_cli-0.14.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/youtube.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|