upload-post 2.8.1__tar.gz → 2.9.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.
- {upload_post-2.8.1 → upload_post-2.9.0}/PKG-INFO +80 -2
- {upload_post-2.8.1 → upload_post-2.9.0}/README.md +79 -1
- {upload_post-2.8.1 → upload_post-2.9.0}/setup.py +1 -1
- {upload_post-2.8.1 → upload_post-2.9.0}/upload_post/__init__.py +1 -1
- {upload_post-2.8.1 → upload_post-2.9.0}/upload_post/api_client.py +169 -15
- {upload_post-2.8.1 → upload_post-2.9.0}/upload_post.egg-info/PKG-INFO +80 -2
- {upload_post-2.8.1 → upload_post-2.9.0}/setup.cfg +0 -0
- {upload_post-2.8.1 → upload_post-2.9.0}/upload_post/cli.py +0 -0
- {upload_post-2.8.1 → upload_post-2.9.0}/upload_post.egg-info/SOURCES.txt +0 -0
- {upload_post-2.8.1 → upload_post-2.9.0}/upload_post.egg-info/dependency_links.txt +0 -0
- {upload_post-2.8.1 → upload_post-2.9.0}/upload_post.egg-info/requires.txt +0 -0
- {upload_post-2.8.1 → upload_post-2.9.0}/upload_post.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: upload-post
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.9.0
|
|
4
4
|
Summary: Cross-platform social media upload for TikTok, Instagram, YouTube, LinkedIn, Facebook, Pinterest, Threads, Reddit, Bluesky, Discord, Telegram, and X (Twitter)
|
|
5
5
|
Home-page: https://www.upload-post.com/
|
|
6
6
|
Author: Upload-Post
|
|
@@ -211,9 +211,16 @@ jwt = client.generate_jwt(
|
|
|
211
211
|
redirect_url="https://yourapp.com/callback",
|
|
212
212
|
platforms=["tiktok", "instagram"],
|
|
213
213
|
# Optional: force the connection page language for this profile.
|
|
214
|
-
# Supported: "en", "es", "de", "fr", "pt". When omitted, the page
|
|
214
|
+
# Supported: "en", "es", "de", "fr", "pt", "pl", "tr". When omitted, the page
|
|
215
215
|
# auto-detects the visitor's browser language and falls back to English.
|
|
216
216
|
language="es",
|
|
217
|
+
# Optional: override individual connection-page strings. Flat dict of i18n
|
|
218
|
+
# dot-path keys to strings. Max 100 entries, keys ^[a-zA-Z0-9_.]+$, values
|
|
219
|
+
# up to 300 chars. Echoed back in the "profile" object of validate_jwt.
|
|
220
|
+
ui_labels={
|
|
221
|
+
"connect.title": "Link your accounts",
|
|
222
|
+
"connect.subtitle": "Publish everywhere from one place",
|
|
223
|
+
},
|
|
217
224
|
)
|
|
218
225
|
```
|
|
219
226
|
|
|
@@ -225,6 +232,35 @@ analytics = client.get_analytics(
|
|
|
225
232
|
platforms=["instagram", "tiktok"],
|
|
226
233
|
)
|
|
227
234
|
print(analytics)
|
|
235
|
+
|
|
236
|
+
# Instagram returns two audience breakdowns with the same shape
|
|
237
|
+
# ("age", "gender", "country", "city"):
|
|
238
|
+
print(analytics["analytics"]["instagram"]["follower_demographics"])
|
|
239
|
+
print(analytics["analytics"]["instagram"]["engaged_audience_demographics"])
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Cached Post Analytics
|
|
243
|
+
|
|
244
|
+
Replays per-post metrics already fetched, instead of calling the platforms again. Only contains posts previously fetched through a live per-post endpoint; there is no background refresh, so captured_at is the last time that post was read live. Not subject to the live
|
|
245
|
+
calls, so it is not subject to the live post-analytics rate limit
|
|
246
|
+
(100 requests / 5 minutes). Use it to page through a profile's post history.
|
|
247
|
+
|
|
248
|
+
```python
|
|
249
|
+
cursor = None
|
|
250
|
+
while True:
|
|
251
|
+
page = client.get_cached_post_analytics(
|
|
252
|
+
"my-profile",
|
|
253
|
+
platform="youtube", # optional: instagram, tiktok, youtube, facebook, linkedin, threads, pinterest, reddit
|
|
254
|
+
limit=50, # default 50, max 200
|
|
255
|
+
since="2026-06-01", # defaults to 30 days ago
|
|
256
|
+
until="2026-07-01", # defaults to today
|
|
257
|
+
cursor=cursor,
|
|
258
|
+
)
|
|
259
|
+
for post in page["posts"]:
|
|
260
|
+
print(post["platform"], post["post_id"], post["metrics"])
|
|
261
|
+
cursor = page["next_cursor"]
|
|
262
|
+
if not cursor:
|
|
263
|
+
break
|
|
228
264
|
```
|
|
229
265
|
|
|
230
266
|
### Get Media
|
|
@@ -244,6 +280,24 @@ media = client.get_media("linkedin", "my-profile", page_urn="me")
|
|
|
244
280
|
media = client.get_media("linkedin", "my-profile", page_urn="12345")
|
|
245
281
|
```
|
|
246
282
|
|
|
283
|
+
The response carries a `pagination` object — `{"limit": ..., "next_cursor": ...,
|
|
284
|
+
"has_more": ...}`, with `next_cursor` `None` and `has_more` `False` on the last
|
|
285
|
+
page:
|
|
286
|
+
|
|
287
|
+
```python
|
|
288
|
+
cursor = None
|
|
289
|
+
while True:
|
|
290
|
+
page = client.get_media("instagram", "my-profile", limit=50, cursor=cursor)
|
|
291
|
+
print(len(page["media"]))
|
|
292
|
+
cursor = page["pagination"]["next_cursor"]
|
|
293
|
+
if not cursor:
|
|
294
|
+
break
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
`limit` defaults to 25 and is clamped to 1-100, with per-platform caps of 20 for
|
|
298
|
+
TikTok and 50 for YouTube. **LinkedIn, Discord and Telegram do not support
|
|
299
|
+
cursors** — they accept `limit` only, and passing a `cursor` returns HTTP 400.
|
|
300
|
+
|
|
247
301
|
### Helper Methods
|
|
248
302
|
|
|
249
303
|
```python
|
|
@@ -344,6 +398,30 @@ boards = client.get_pinterest_boards("my-profile")
|
|
|
344
398
|
- `subreddit` - Subreddit name (without r/)
|
|
345
399
|
- `flair_id` - Flair template ID
|
|
346
400
|
|
|
401
|
+
### Google Business
|
|
402
|
+
- `gbp_location_id` - Location, e.g. `"accounts/123/locations/456"` (list them with `get_google_business_locations`). Required when the account has more than one location; the API only auto-selects when exactly one exists.
|
|
403
|
+
- `gbp_post_type` - `MEDIA`, `PHOTO` or `GALLERY` to publish into the location's photo gallery instead of creating a Local Post. Any other value, or omitting it, keeps the Local Post behaviour.
|
|
404
|
+
- `gbp_media_category` - Gallery category, default `ADDITIONAL`. One of `COVER`, `PROFILE`, `LOGO`, `EXTERIOR`, `INTERIOR`, `PRODUCT`, `AT_WORK`, `FOOD_AND_DRINK`, `MENU`, `COMMON_AREA`, `ROOMS`, `TEAMS`, `ADDITIONAL`.
|
|
405
|
+
- `gbp_topic_type` - STANDARD, EVENT or OFFER
|
|
406
|
+
- `gbp_media_url` / `gbp_media_format` - Media attached to the post
|
|
407
|
+
- `gbp_cta_type` / `gbp_cta_url` - Call-to-action button
|
|
408
|
+
- `gbp_event_title` / `gbp_event_start_date` / `gbp_event_start_time` / `gbp_event_end_date` / `gbp_event_end_time` - Used with `gbp_topic_type="EVENT"`
|
|
409
|
+
- `gbp_offer_coupon` / `gbp_offer_redeem_url` / `gbp_offer_terms` - Used with `gbp_topic_type="OFFER"`
|
|
410
|
+
|
|
411
|
+
```python
|
|
412
|
+
locations = client.get_google_business_locations("my-profile")
|
|
413
|
+
|
|
414
|
+
# Publish a photo straight into the location's gallery
|
|
415
|
+
client.upload_photos(
|
|
416
|
+
["storefront.jpg"],
|
|
417
|
+
user="my-profile",
|
|
418
|
+
platforms=["google_business"],
|
|
419
|
+
gbp_location_id=locations["locations"][0]["name"],
|
|
420
|
+
gbp_post_type="GALLERY",
|
|
421
|
+
gbp_media_category="EXTERIOR",
|
|
422
|
+
)
|
|
423
|
+
```
|
|
424
|
+
|
|
347
425
|
## Common Options
|
|
348
426
|
|
|
349
427
|
These options work across all upload methods:
|
|
@@ -173,9 +173,16 @@ jwt = client.generate_jwt(
|
|
|
173
173
|
redirect_url="https://yourapp.com/callback",
|
|
174
174
|
platforms=["tiktok", "instagram"],
|
|
175
175
|
# Optional: force the connection page language for this profile.
|
|
176
|
-
# Supported: "en", "es", "de", "fr", "pt". When omitted, the page
|
|
176
|
+
# Supported: "en", "es", "de", "fr", "pt", "pl", "tr". When omitted, the page
|
|
177
177
|
# auto-detects the visitor's browser language and falls back to English.
|
|
178
178
|
language="es",
|
|
179
|
+
# Optional: override individual connection-page strings. Flat dict of i18n
|
|
180
|
+
# dot-path keys to strings. Max 100 entries, keys ^[a-zA-Z0-9_.]+$, values
|
|
181
|
+
# up to 300 chars. Echoed back in the "profile" object of validate_jwt.
|
|
182
|
+
ui_labels={
|
|
183
|
+
"connect.title": "Link your accounts",
|
|
184
|
+
"connect.subtitle": "Publish everywhere from one place",
|
|
185
|
+
},
|
|
179
186
|
)
|
|
180
187
|
```
|
|
181
188
|
|
|
@@ -187,6 +194,35 @@ analytics = client.get_analytics(
|
|
|
187
194
|
platforms=["instagram", "tiktok"],
|
|
188
195
|
)
|
|
189
196
|
print(analytics)
|
|
197
|
+
|
|
198
|
+
# Instagram returns two audience breakdowns with the same shape
|
|
199
|
+
# ("age", "gender", "country", "city"):
|
|
200
|
+
print(analytics["analytics"]["instagram"]["follower_demographics"])
|
|
201
|
+
print(analytics["analytics"]["instagram"]["engaged_audience_demographics"])
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Cached Post Analytics
|
|
205
|
+
|
|
206
|
+
Replays per-post metrics already fetched, instead of calling the platforms again. Only contains posts previously fetched through a live per-post endpoint; there is no background refresh, so captured_at is the last time that post was read live. Not subject to the live
|
|
207
|
+
calls, so it is not subject to the live post-analytics rate limit
|
|
208
|
+
(100 requests / 5 minutes). Use it to page through a profile's post history.
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
cursor = None
|
|
212
|
+
while True:
|
|
213
|
+
page = client.get_cached_post_analytics(
|
|
214
|
+
"my-profile",
|
|
215
|
+
platform="youtube", # optional: instagram, tiktok, youtube, facebook, linkedin, threads, pinterest, reddit
|
|
216
|
+
limit=50, # default 50, max 200
|
|
217
|
+
since="2026-06-01", # defaults to 30 days ago
|
|
218
|
+
until="2026-07-01", # defaults to today
|
|
219
|
+
cursor=cursor,
|
|
220
|
+
)
|
|
221
|
+
for post in page["posts"]:
|
|
222
|
+
print(post["platform"], post["post_id"], post["metrics"])
|
|
223
|
+
cursor = page["next_cursor"]
|
|
224
|
+
if not cursor:
|
|
225
|
+
break
|
|
190
226
|
```
|
|
191
227
|
|
|
192
228
|
### Get Media
|
|
@@ -206,6 +242,24 @@ media = client.get_media("linkedin", "my-profile", page_urn="me")
|
|
|
206
242
|
media = client.get_media("linkedin", "my-profile", page_urn="12345")
|
|
207
243
|
```
|
|
208
244
|
|
|
245
|
+
The response carries a `pagination` object — `{"limit": ..., "next_cursor": ...,
|
|
246
|
+
"has_more": ...}`, with `next_cursor` `None` and `has_more` `False` on the last
|
|
247
|
+
page:
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
cursor = None
|
|
251
|
+
while True:
|
|
252
|
+
page = client.get_media("instagram", "my-profile", limit=50, cursor=cursor)
|
|
253
|
+
print(len(page["media"]))
|
|
254
|
+
cursor = page["pagination"]["next_cursor"]
|
|
255
|
+
if not cursor:
|
|
256
|
+
break
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
`limit` defaults to 25 and is clamped to 1-100, with per-platform caps of 20 for
|
|
260
|
+
TikTok and 50 for YouTube. **LinkedIn, Discord and Telegram do not support
|
|
261
|
+
cursors** — they accept `limit` only, and passing a `cursor` returns HTTP 400.
|
|
262
|
+
|
|
209
263
|
### Helper Methods
|
|
210
264
|
|
|
211
265
|
```python
|
|
@@ -306,6 +360,30 @@ boards = client.get_pinterest_boards("my-profile")
|
|
|
306
360
|
- `subreddit` - Subreddit name (without r/)
|
|
307
361
|
- `flair_id` - Flair template ID
|
|
308
362
|
|
|
363
|
+
### Google Business
|
|
364
|
+
- `gbp_location_id` - Location, e.g. `"accounts/123/locations/456"` (list them with `get_google_business_locations`). Required when the account has more than one location; the API only auto-selects when exactly one exists.
|
|
365
|
+
- `gbp_post_type` - `MEDIA`, `PHOTO` or `GALLERY` to publish into the location's photo gallery instead of creating a Local Post. Any other value, or omitting it, keeps the Local Post behaviour.
|
|
366
|
+
- `gbp_media_category` - Gallery category, default `ADDITIONAL`. One of `COVER`, `PROFILE`, `LOGO`, `EXTERIOR`, `INTERIOR`, `PRODUCT`, `AT_WORK`, `FOOD_AND_DRINK`, `MENU`, `COMMON_AREA`, `ROOMS`, `TEAMS`, `ADDITIONAL`.
|
|
367
|
+
- `gbp_topic_type` - STANDARD, EVENT or OFFER
|
|
368
|
+
- `gbp_media_url` / `gbp_media_format` - Media attached to the post
|
|
369
|
+
- `gbp_cta_type` / `gbp_cta_url` - Call-to-action button
|
|
370
|
+
- `gbp_event_title` / `gbp_event_start_date` / `gbp_event_start_time` / `gbp_event_end_date` / `gbp_event_end_time` - Used with `gbp_topic_type="EVENT"`
|
|
371
|
+
- `gbp_offer_coupon` / `gbp_offer_redeem_url` / `gbp_offer_terms` - Used with `gbp_topic_type="OFFER"`
|
|
372
|
+
|
|
373
|
+
```python
|
|
374
|
+
locations = client.get_google_business_locations("my-profile")
|
|
375
|
+
|
|
376
|
+
# Publish a photo straight into the location's gallery
|
|
377
|
+
client.upload_photos(
|
|
378
|
+
["storefront.jpg"],
|
|
379
|
+
user="my-profile",
|
|
380
|
+
platforms=["google_business"],
|
|
381
|
+
gbp_location_id=locations["locations"][0]["name"],
|
|
382
|
+
gbp_post_type="GALLERY",
|
|
383
|
+
gbp_media_category="EXTERIOR",
|
|
384
|
+
)
|
|
385
|
+
```
|
|
386
|
+
|
|
309
387
|
## Common Options
|
|
310
388
|
|
|
311
389
|
These options work across all upload methods:
|
|
@@ -5,7 +5,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
|
|
|
5
5
|
|
|
6
6
|
setup(
|
|
7
7
|
name="upload-post",
|
|
8
|
-
version="2.
|
|
8
|
+
version="2.9.0",
|
|
9
9
|
author="Upload-Post",
|
|
10
10
|
author_email="hi@img2html.com",
|
|
11
11
|
description="Cross-platform social media upload for TikTok, Instagram, YouTube, LinkedIn, Facebook, Pinterest, Threads, Reddit, Bluesky, Discord, Telegram, and X (Twitter)",
|
|
@@ -400,6 +400,49 @@ class UploadPostClient:
|
|
|
400
400
|
if reddit_link:
|
|
401
401
|
data.append(("reddit_link_url", reddit_link))
|
|
402
402
|
|
|
403
|
+
def _add_google_business_params(self, data: List[tuple], **kwargs):
|
|
404
|
+
"""Add Google Business Profile parameters.
|
|
405
|
+
|
|
406
|
+
``gbp_location_id`` is required for accounts with more than one location;
|
|
407
|
+
the API only auto-selects when exactly one location exists.
|
|
408
|
+
|
|
409
|
+
``gbp_post_type`` switches the call from the default Local Post to a
|
|
410
|
+
gallery photo upload (MEDIA / PHOTO / GALLERY). Any other value, or
|
|
411
|
+
omitting it, keeps the Local Post behaviour.
|
|
412
|
+
"""
|
|
413
|
+
if kwargs.get("gbp_location_id"):
|
|
414
|
+
data.append(("gbp_location_id", kwargs["gbp_location_id"]))
|
|
415
|
+
if kwargs.get("gbp_post_type"):
|
|
416
|
+
data.append(("gbp_post_type", str(kwargs["gbp_post_type"]).upper()))
|
|
417
|
+
if kwargs.get("gbp_media_category"):
|
|
418
|
+
data.append(("gbp_media_category", str(kwargs["gbp_media_category"]).upper()))
|
|
419
|
+
if kwargs.get("gbp_topic_type"):
|
|
420
|
+
data.append(("gbp_topic_type", str(kwargs["gbp_topic_type"]).upper()))
|
|
421
|
+
if kwargs.get("gbp_media_url"):
|
|
422
|
+
data.append(("gbp_media_url", kwargs["gbp_media_url"]))
|
|
423
|
+
if kwargs.get("gbp_media_format"):
|
|
424
|
+
data.append(("gbp_media_format", str(kwargs["gbp_media_format"]).upper()))
|
|
425
|
+
if kwargs.get("gbp_cta_type"):
|
|
426
|
+
data.append(("gbp_cta_type", str(kwargs["gbp_cta_type"]).upper()))
|
|
427
|
+
if kwargs.get("gbp_cta_url"):
|
|
428
|
+
data.append(("gbp_cta_url", kwargs["gbp_cta_url"]))
|
|
429
|
+
if kwargs.get("gbp_event_title"):
|
|
430
|
+
data.append(("gbp_event_title", kwargs["gbp_event_title"]))
|
|
431
|
+
if kwargs.get("gbp_event_start_date"):
|
|
432
|
+
data.append(("gbp_event_start_date", kwargs["gbp_event_start_date"]))
|
|
433
|
+
if kwargs.get("gbp_event_start_time"):
|
|
434
|
+
data.append(("gbp_event_start_time", kwargs["gbp_event_start_time"]))
|
|
435
|
+
if kwargs.get("gbp_event_end_date"):
|
|
436
|
+
data.append(("gbp_event_end_date", kwargs["gbp_event_end_date"]))
|
|
437
|
+
if kwargs.get("gbp_event_end_time"):
|
|
438
|
+
data.append(("gbp_event_end_time", kwargs["gbp_event_end_time"]))
|
|
439
|
+
if kwargs.get("gbp_offer_coupon"):
|
|
440
|
+
data.append(("gbp_offer_coupon", kwargs["gbp_offer_coupon"]))
|
|
441
|
+
if kwargs.get("gbp_offer_redeem_url"):
|
|
442
|
+
data.append(("gbp_offer_redeem_url", kwargs["gbp_offer_redeem_url"]))
|
|
443
|
+
if kwargs.get("gbp_offer_terms"):
|
|
444
|
+
data.append(("gbp_offer_terms", kwargs["gbp_offer_terms"]))
|
|
445
|
+
|
|
403
446
|
def upload_video(
|
|
404
447
|
self,
|
|
405
448
|
video_path: Union[str, Path],
|
|
@@ -418,7 +461,7 @@ class UploadPostClient:
|
|
|
418
461
|
user: User identifier (profile name).
|
|
419
462
|
platforms: Target platforms. Supported: tiktok, instagram, youtube,
|
|
420
463
|
linkedin, facebook, pinterest, threads, bluesky, discord, telegram, x,
|
|
421
|
-
mastodon, wordpress
|
|
464
|
+
google_business, mastodon, wordpress
|
|
422
465
|
|
|
423
466
|
Keyword Args:
|
|
424
467
|
description: Video description
|
|
@@ -427,10 +470,7 @@ class UploadPostClient:
|
|
|
427
470
|
timezone: Timezone for scheduled date (e.g., "Europe/Madrid")
|
|
428
471
|
add_to_queue: Add to posting queue
|
|
429
472
|
async_upload: Process asynchronously (default: True)
|
|
430
|
-
autogenerate: If True, the server uses AI to generate native per-platform
|
|
431
473
|
title/description from the media and fills any platform field
|
|
432
|
-
left empty. Also: autogenerate_title / autogenerate_description
|
|
433
|
-
(bool) for granularity, autogenerate_language (ISO code) to force
|
|
434
474
|
the language (omit to auto-detect from the media).
|
|
435
475
|
|
|
436
476
|
TikTok:
|
|
@@ -507,6 +547,22 @@ class UploadPostClient:
|
|
|
507
547
|
threads_long_text_as_post: Post long text as single post
|
|
508
548
|
threads_topic_tag: Topic tag for the post (1-50 chars, no periods or ampersands)
|
|
509
549
|
|
|
550
|
+
Google Business:
|
|
551
|
+
gbp_location_id: Location, e.g. "accounts/123/locations/456". Required
|
|
552
|
+
when the account has more than one location.
|
|
553
|
+
gbp_post_type: MEDIA, PHOTO or GALLERY to publish into the location's
|
|
554
|
+
gallery instead of creating a Local Post. Any other value, or
|
|
555
|
+
omitting it, keeps the Local Post behaviour.
|
|
556
|
+
gbp_media_category: Gallery category (default ADDITIONAL). One of
|
|
557
|
+
COVER, PROFILE, LOGO, EXTERIOR, INTERIOR, PRODUCT, AT_WORK,
|
|
558
|
+
FOOD_AND_DRINK, MENU, COMMON_AREA, ROOMS, TEAMS, ADDITIONAL.
|
|
559
|
+
gbp_topic_type: STANDARD, EVENT or OFFER
|
|
560
|
+
gbp_media_url / gbp_media_format: Media attached to the post
|
|
561
|
+
gbp_cta_type / gbp_cta_url: Call-to-action button
|
|
562
|
+
gbp_event_title / gbp_event_start_date / gbp_event_start_time /
|
|
563
|
+
gbp_event_end_date / gbp_event_end_time: Used with EVENT
|
|
564
|
+
gbp_offer_coupon / gbp_offer_redeem_url / gbp_offer_terms: Used with OFFER
|
|
565
|
+
|
|
510
566
|
Returns:
|
|
511
567
|
API response with request_id for async uploads.
|
|
512
568
|
|
|
@@ -546,7 +602,9 @@ class UploadPostClient:
|
|
|
546
602
|
self._add_x_params(data, is_text=False, **kwargs)
|
|
547
603
|
if "threads" in platforms:
|
|
548
604
|
self._add_threads_params(data, **kwargs)
|
|
549
|
-
|
|
605
|
+
if "google_business" in platforms:
|
|
606
|
+
self._add_google_business_params(data, **kwargs)
|
|
607
|
+
|
|
550
608
|
return self._request("/upload", "POST", data=data, files=files if files else None)
|
|
551
609
|
|
|
552
610
|
finally:
|
|
@@ -571,7 +629,7 @@ class UploadPostClient:
|
|
|
571
629
|
user: User identifier (profile name).
|
|
572
630
|
platforms: Target platforms. Supported: tiktok, instagram, linkedin,
|
|
573
631
|
facebook, pinterest, threads, reddit, bluesky, discord, telegram, x,
|
|
574
|
-
mastodon, lemmy, wordpress
|
|
632
|
+
google_business, mastodon, lemmy, wordpress
|
|
575
633
|
|
|
576
634
|
Keyword Args:
|
|
577
635
|
description: Photo description
|
|
@@ -630,6 +688,18 @@ class UploadPostClient:
|
|
|
630
688
|
subreddit: Subreddit name (without r/)
|
|
631
689
|
flair_id: Flair template ID
|
|
632
690
|
|
|
691
|
+
Google Business:
|
|
692
|
+
gbp_location_id: Location, e.g. "accounts/123/locations/456". Required
|
|
693
|
+
when the account has more than one location.
|
|
694
|
+
gbp_post_type: MEDIA, PHOTO or GALLERY to publish the photo into the
|
|
695
|
+
location's gallery instead of creating a Local Post. Any other
|
|
696
|
+
value, or omitting it, keeps the Local Post behaviour.
|
|
697
|
+
gbp_media_category: Gallery category (default ADDITIONAL). One of
|
|
698
|
+
COVER, PROFILE, LOGO, EXTERIOR, INTERIOR, PRODUCT, AT_WORK,
|
|
699
|
+
FOOD_AND_DRINK, MENU, COMMON_AREA, ROOMS, TEAMS, ADDITIONAL.
|
|
700
|
+
gbp_topic_type: STANDARD, EVENT or OFFER
|
|
701
|
+
gbp_cta_type / gbp_cta_url: Call-to-action button
|
|
702
|
+
|
|
633
703
|
first_comment_media: List of file paths to attach as images in
|
|
634
704
|
the first comment. Supported on Reddit and X.
|
|
635
705
|
|
|
@@ -674,6 +744,8 @@ class UploadPostClient:
|
|
|
674
744
|
self._add_threads_params(data, **kwargs)
|
|
675
745
|
if "reddit" in platforms:
|
|
676
746
|
self._add_reddit_params(data, **kwargs)
|
|
747
|
+
if "google_business" in platforms:
|
|
748
|
+
self._add_google_business_params(data, **kwargs)
|
|
677
749
|
|
|
678
750
|
first_comment_media = kwargs.get("first_comment_media")
|
|
679
751
|
if first_comment_media:
|
|
@@ -705,8 +777,9 @@ class UploadPostClient:
|
|
|
705
777
|
title: Text content for the post.
|
|
706
778
|
user: User identifier (profile name).
|
|
707
779
|
platforms: Target platforms. Supported: x, linkedin, facebook,
|
|
708
|
-
threads, reddit, bluesky, discord, telegram,
|
|
709
|
-
nostr, lemmy, devto, hashnode, wordpress,
|
|
780
|
+
threads, reddit, bluesky, discord, telegram, google_business,
|
|
781
|
+
slack, mastodon, nostr, lemmy, devto, hashnode, wordpress,
|
|
782
|
+
whop, listmonk
|
|
710
783
|
|
|
711
784
|
Keyword Args:
|
|
712
785
|
first_comment: First comment to post
|
|
@@ -749,6 +822,15 @@ class UploadPostClient:
|
|
|
749
822
|
(kind: "link") instead of a text post. Overrides `link_url`
|
|
750
823
|
for Reddit.
|
|
751
824
|
|
|
825
|
+
Google Business:
|
|
826
|
+
gbp_location_id: Location, e.g. "accounts/123/locations/456". Required
|
|
827
|
+
when the account has more than one location.
|
|
828
|
+
gbp_topic_type: STANDARD, EVENT or OFFER
|
|
829
|
+
gbp_cta_type / gbp_cta_url: Call-to-action button
|
|
830
|
+
gbp_event_title / gbp_event_start_date / gbp_event_start_time /
|
|
831
|
+
gbp_event_end_date / gbp_event_end_time: Used with EVENT
|
|
832
|
+
gbp_offer_coupon / gbp_offer_redeem_url / gbp_offer_terms: Used with OFFER
|
|
833
|
+
|
|
752
834
|
first_comment_media: List of file paths to attach as images in
|
|
753
835
|
the first comment. Supported on Reddit and X.
|
|
754
836
|
|
|
@@ -781,6 +863,8 @@ class UploadPostClient:
|
|
|
781
863
|
bluesky_link = kwargs.get("bluesky_link_url")
|
|
782
864
|
if bluesky_link:
|
|
783
865
|
data.append(("bluesky_link_url", bluesky_link))
|
|
866
|
+
if "google_business" in platforms:
|
|
867
|
+
self._add_google_business_params(data, **kwargs)
|
|
784
868
|
|
|
785
869
|
first_comment_media = kwargs.get("first_comment_media")
|
|
786
870
|
opened_files: List = []
|
|
@@ -976,7 +1060,11 @@ class UploadPostClient:
|
|
|
976
1060
|
|
|
977
1061
|
Returns:
|
|
978
1062
|
Analytics data per platform. For Instagram, the response includes both
|
|
979
|
-
'views' (official Instagram metric) and 'impressions' (backwards-compatible alias)
|
|
1063
|
+
'views' (official Instagram metric) and 'impressions' (backwards-compatible alias),
|
|
1064
|
+
plus two audience breakdowns with the same shape ('age', 'gender',
|
|
1065
|
+
'country', 'city'): 'follower_demographics' for the account's followers
|
|
1066
|
+
and 'engaged_audience_demographics' for the accounts that engaged with
|
|
1067
|
+
its content.
|
|
980
1068
|
"""
|
|
981
1069
|
params = {}
|
|
982
1070
|
if platforms:
|
|
@@ -1062,6 +1150,49 @@ class UploadPostClient:
|
|
|
1062
1150
|
}
|
|
1063
1151
|
return self._request("/uploadposts/post-analytics", "GET", params=params)
|
|
1064
1152
|
|
|
1153
|
+
def get_cached_post_analytics(
|
|
1154
|
+
self,
|
|
1155
|
+
user: str,
|
|
1156
|
+
platform: Optional[str] = None,
|
|
1157
|
+
limit: Optional[int] = None,
|
|
1158
|
+
cursor: Optional[str] = None,
|
|
1159
|
+
since: Optional[str] = None,
|
|
1160
|
+
until: Optional[str] = None,
|
|
1161
|
+
) -> Dict:
|
|
1162
|
+
"""
|
|
1163
|
+
Replay per-post metrics already fetched, instead of querying
|
|
1164
|
+
the platforms live.
|
|
1165
|
+
|
|
1166
|
+
Because it never calls the platform APIs it is not subject to the live
|
|
1167
|
+
post-analytics rate limit (100 requests / 5 minutes), which makes it the
|
|
1168
|
+
right method for paging through a profile's whole post history.
|
|
1169
|
+
|
|
1170
|
+
Args:
|
|
1171
|
+
user: Profile username.
|
|
1172
|
+
platform: Filter by platform (instagram, tiktok, youtube, facebook,
|
|
1173
|
+
linkedin, threads, pinterest, reddit).
|
|
1174
|
+
limit: Posts per page. Defaults to 50, max 200.
|
|
1175
|
+
cursor: Opaque cursor from a previous response's ``next_cursor``.
|
|
1176
|
+
since: Start date in YYYY-MM-DD format (defaults to 30 days ago).
|
|
1177
|
+
until: End date in YYYY-MM-DD format (defaults to today).
|
|
1178
|
+
|
|
1179
|
+
Returns:
|
|
1180
|
+
Cached post metrics with ``source: "snapshot_cache"``, plus
|
|
1181
|
+
``next_cursor`` and ``has_more`` for pagination.
|
|
1182
|
+
"""
|
|
1183
|
+
params: Dict[str, Any] = {"user": user}
|
|
1184
|
+
if platform:
|
|
1185
|
+
params["platform"] = platform
|
|
1186
|
+
if limit is not None:
|
|
1187
|
+
params["limit"] = limit
|
|
1188
|
+
if cursor:
|
|
1189
|
+
params["cursor"] = cursor
|
|
1190
|
+
if since:
|
|
1191
|
+
params["since"] = since
|
|
1192
|
+
if until:
|
|
1193
|
+
params["until"] = until
|
|
1194
|
+
return self._request("/uploadposts/post-analytics/cached", "GET", params=params)
|
|
1195
|
+
|
|
1065
1196
|
def get_platform_metrics(self) -> Dict:
|
|
1066
1197
|
"""
|
|
1067
1198
|
Get available metrics configuration for all supported platforms.
|
|
@@ -1076,6 +1207,8 @@ class UploadPostClient:
|
|
|
1076
1207
|
platform: str,
|
|
1077
1208
|
user: str,
|
|
1078
1209
|
page_urn: Optional[str] = None,
|
|
1210
|
+
limit: Optional[int] = None,
|
|
1211
|
+
cursor: Optional[str] = None,
|
|
1079
1212
|
) -> Dict:
|
|
1080
1213
|
"""
|
|
1081
1214
|
Retrieve recent media from a connected social account.
|
|
@@ -1086,13 +1219,24 @@ class UploadPostClient:
|
|
|
1086
1219
|
user: Profile username.
|
|
1087
1220
|
page_urn: LinkedIn only. Numeric org ID, full URN, or ``"me"`` to
|
|
1088
1221
|
force the personal profile of an org-admin account.
|
|
1222
|
+
limit: Items per page. Defaults to 25 and is clamped to 1-100.
|
|
1223
|
+
Per-platform caps: TikTok 20, YouTube 50, everything else 100.
|
|
1224
|
+
cursor: Opaque cursor from a previous response's
|
|
1225
|
+
``pagination["next_cursor"]``. LinkedIn, Discord and Telegram do
|
|
1226
|
+
not support cursors: passing one there returns HTTP 400.
|
|
1089
1227
|
|
|
1090
1228
|
Returns:
|
|
1091
|
-
``{"success": True, "media": [...]
|
|
1229
|
+
``{"success": True, "media": [...], "pagination": {"limit": 50,
|
|
1230
|
+
"next_cursor": "QVFI...", "has_more": True}}``. The last page has
|
|
1231
|
+
``next_cursor`` ``None`` and ``has_more`` ``False``.
|
|
1092
1232
|
"""
|
|
1093
|
-
params: Dict[str,
|
|
1233
|
+
params: Dict[str, Any] = {"platform": platform, "user": user}
|
|
1094
1234
|
if page_urn:
|
|
1095
1235
|
params["page_urn"] = page_urn
|
|
1236
|
+
if limit is not None:
|
|
1237
|
+
params["limit"] = limit
|
|
1238
|
+
if cursor:
|
|
1239
|
+
params["cursor"] = cursor
|
|
1096
1240
|
return self._request("/uploadposts/media", "GET", params=params)
|
|
1097
1241
|
|
|
1098
1242
|
# ==================== Scheduled Posts ====================
|
|
@@ -1188,7 +1332,8 @@ class UploadPostClient:
|
|
|
1188
1332
|
readonly_calendar: Optional[bool] = None,
|
|
1189
1333
|
connect_title: Optional[str] = None,
|
|
1190
1334
|
connect_description: Optional[str] = None,
|
|
1191
|
-
language: Optional[str] = None
|
|
1335
|
+
language: Optional[str] = None,
|
|
1336
|
+
ui_labels: Optional[Dict[str, str]] = None
|
|
1192
1337
|
) -> Dict:
|
|
1193
1338
|
"""
|
|
1194
1339
|
Generate a JWT for platform integration.
|
|
@@ -1205,8 +1350,13 @@ class UploadPostClient:
|
|
|
1205
1350
|
connect_title: Custom title for the connection page.
|
|
1206
1351
|
connect_description: Custom description for the connection page.
|
|
1207
1352
|
language: Force the connection page language for this profile.
|
|
1208
|
-
Supported: 'en', 'es', 'de', 'fr', 'pt'. When omitted,
|
|
1209
|
-
auto-detects the visitor's browser language and falls back
|
|
1353
|
+
Supported: 'en', 'es', 'de', 'fr', 'pt', 'pl', 'tr'. When omitted,
|
|
1354
|
+
the page auto-detects the visitor's browser language and falls back
|
|
1355
|
+
to English.
|
|
1356
|
+
ui_labels: Flat mapping of i18n dot-path keys to replacement strings,
|
|
1357
|
+
e.g. {"connect.title": "Link your accounts"}. Max 100 entries; keys
|
|
1358
|
+
must match ^[a-zA-Z0-9_.]+$ and values are strings of up to 300
|
|
1359
|
+
characters. Echoed back in the 'profile' object of validate_jwt.
|
|
1210
1360
|
|
|
1211
1361
|
Returns:
|
|
1212
1362
|
JWT and connection URL.
|
|
@@ -1230,6 +1380,8 @@ class UploadPostClient:
|
|
|
1230
1380
|
body["connect_description"] = connect_description
|
|
1231
1381
|
if language:
|
|
1232
1382
|
body["language"] = language
|
|
1383
|
+
if ui_labels:
|
|
1384
|
+
body["ui_labels"] = ui_labels
|
|
1233
1385
|
return self._request("/uploadposts/users/generate-jwt", "POST", json_data=body)
|
|
1234
1386
|
|
|
1235
1387
|
def validate_jwt(self, jwt: str) -> Dict:
|
|
@@ -1240,7 +1392,9 @@ class UploadPostClient:
|
|
|
1240
1392
|
jwt: JWT token to validate.
|
|
1241
1393
|
|
|
1242
1394
|
Returns:
|
|
1243
|
-
Validation result.
|
|
1395
|
+
Validation result. The 'profile' object echoes back the connection
|
|
1396
|
+
page settings, including 'language' and any 'ui_labels' sent to
|
|
1397
|
+
generate_jwt.
|
|
1244
1398
|
"""
|
|
1245
1399
|
return self._request("/uploadposts/users/validate-jwt", "POST", json_data={"jwt": jwt})
|
|
1246
1400
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: upload-post
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.9.0
|
|
4
4
|
Summary: Cross-platform social media upload for TikTok, Instagram, YouTube, LinkedIn, Facebook, Pinterest, Threads, Reddit, Bluesky, Discord, Telegram, and X (Twitter)
|
|
5
5
|
Home-page: https://www.upload-post.com/
|
|
6
6
|
Author: Upload-Post
|
|
@@ -211,9 +211,16 @@ jwt = client.generate_jwt(
|
|
|
211
211
|
redirect_url="https://yourapp.com/callback",
|
|
212
212
|
platforms=["tiktok", "instagram"],
|
|
213
213
|
# Optional: force the connection page language for this profile.
|
|
214
|
-
# Supported: "en", "es", "de", "fr", "pt". When omitted, the page
|
|
214
|
+
# Supported: "en", "es", "de", "fr", "pt", "pl", "tr". When omitted, the page
|
|
215
215
|
# auto-detects the visitor's browser language and falls back to English.
|
|
216
216
|
language="es",
|
|
217
|
+
# Optional: override individual connection-page strings. Flat dict of i18n
|
|
218
|
+
# dot-path keys to strings. Max 100 entries, keys ^[a-zA-Z0-9_.]+$, values
|
|
219
|
+
# up to 300 chars. Echoed back in the "profile" object of validate_jwt.
|
|
220
|
+
ui_labels={
|
|
221
|
+
"connect.title": "Link your accounts",
|
|
222
|
+
"connect.subtitle": "Publish everywhere from one place",
|
|
223
|
+
},
|
|
217
224
|
)
|
|
218
225
|
```
|
|
219
226
|
|
|
@@ -225,6 +232,35 @@ analytics = client.get_analytics(
|
|
|
225
232
|
platforms=["instagram", "tiktok"],
|
|
226
233
|
)
|
|
227
234
|
print(analytics)
|
|
235
|
+
|
|
236
|
+
# Instagram returns two audience breakdowns with the same shape
|
|
237
|
+
# ("age", "gender", "country", "city"):
|
|
238
|
+
print(analytics["analytics"]["instagram"]["follower_demographics"])
|
|
239
|
+
print(analytics["analytics"]["instagram"]["engaged_audience_demographics"])
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Cached Post Analytics
|
|
243
|
+
|
|
244
|
+
Replays per-post metrics already fetched, instead of calling the platforms again. Only contains posts previously fetched through a live per-post endpoint; there is no background refresh, so captured_at is the last time that post was read live. Not subject to the live
|
|
245
|
+
calls, so it is not subject to the live post-analytics rate limit
|
|
246
|
+
(100 requests / 5 minutes). Use it to page through a profile's post history.
|
|
247
|
+
|
|
248
|
+
```python
|
|
249
|
+
cursor = None
|
|
250
|
+
while True:
|
|
251
|
+
page = client.get_cached_post_analytics(
|
|
252
|
+
"my-profile",
|
|
253
|
+
platform="youtube", # optional: instagram, tiktok, youtube, facebook, linkedin, threads, pinterest, reddit
|
|
254
|
+
limit=50, # default 50, max 200
|
|
255
|
+
since="2026-06-01", # defaults to 30 days ago
|
|
256
|
+
until="2026-07-01", # defaults to today
|
|
257
|
+
cursor=cursor,
|
|
258
|
+
)
|
|
259
|
+
for post in page["posts"]:
|
|
260
|
+
print(post["platform"], post["post_id"], post["metrics"])
|
|
261
|
+
cursor = page["next_cursor"]
|
|
262
|
+
if not cursor:
|
|
263
|
+
break
|
|
228
264
|
```
|
|
229
265
|
|
|
230
266
|
### Get Media
|
|
@@ -244,6 +280,24 @@ media = client.get_media("linkedin", "my-profile", page_urn="me")
|
|
|
244
280
|
media = client.get_media("linkedin", "my-profile", page_urn="12345")
|
|
245
281
|
```
|
|
246
282
|
|
|
283
|
+
The response carries a `pagination` object — `{"limit": ..., "next_cursor": ...,
|
|
284
|
+
"has_more": ...}`, with `next_cursor` `None` and `has_more` `False` on the last
|
|
285
|
+
page:
|
|
286
|
+
|
|
287
|
+
```python
|
|
288
|
+
cursor = None
|
|
289
|
+
while True:
|
|
290
|
+
page = client.get_media("instagram", "my-profile", limit=50, cursor=cursor)
|
|
291
|
+
print(len(page["media"]))
|
|
292
|
+
cursor = page["pagination"]["next_cursor"]
|
|
293
|
+
if not cursor:
|
|
294
|
+
break
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
`limit` defaults to 25 and is clamped to 1-100, with per-platform caps of 20 for
|
|
298
|
+
TikTok and 50 for YouTube. **LinkedIn, Discord and Telegram do not support
|
|
299
|
+
cursors** — they accept `limit` only, and passing a `cursor` returns HTTP 400.
|
|
300
|
+
|
|
247
301
|
### Helper Methods
|
|
248
302
|
|
|
249
303
|
```python
|
|
@@ -344,6 +398,30 @@ boards = client.get_pinterest_boards("my-profile")
|
|
|
344
398
|
- `subreddit` - Subreddit name (without r/)
|
|
345
399
|
- `flair_id` - Flair template ID
|
|
346
400
|
|
|
401
|
+
### Google Business
|
|
402
|
+
- `gbp_location_id` - Location, e.g. `"accounts/123/locations/456"` (list them with `get_google_business_locations`). Required when the account has more than one location; the API only auto-selects when exactly one exists.
|
|
403
|
+
- `gbp_post_type` - `MEDIA`, `PHOTO` or `GALLERY` to publish into the location's photo gallery instead of creating a Local Post. Any other value, or omitting it, keeps the Local Post behaviour.
|
|
404
|
+
- `gbp_media_category` - Gallery category, default `ADDITIONAL`. One of `COVER`, `PROFILE`, `LOGO`, `EXTERIOR`, `INTERIOR`, `PRODUCT`, `AT_WORK`, `FOOD_AND_DRINK`, `MENU`, `COMMON_AREA`, `ROOMS`, `TEAMS`, `ADDITIONAL`.
|
|
405
|
+
- `gbp_topic_type` - STANDARD, EVENT or OFFER
|
|
406
|
+
- `gbp_media_url` / `gbp_media_format` - Media attached to the post
|
|
407
|
+
- `gbp_cta_type` / `gbp_cta_url` - Call-to-action button
|
|
408
|
+
- `gbp_event_title` / `gbp_event_start_date` / `gbp_event_start_time` / `gbp_event_end_date` / `gbp_event_end_time` - Used with `gbp_topic_type="EVENT"`
|
|
409
|
+
- `gbp_offer_coupon` / `gbp_offer_redeem_url` / `gbp_offer_terms` - Used with `gbp_topic_type="OFFER"`
|
|
410
|
+
|
|
411
|
+
```python
|
|
412
|
+
locations = client.get_google_business_locations("my-profile")
|
|
413
|
+
|
|
414
|
+
# Publish a photo straight into the location's gallery
|
|
415
|
+
client.upload_photos(
|
|
416
|
+
["storefront.jpg"],
|
|
417
|
+
user="my-profile",
|
|
418
|
+
platforms=["google_business"],
|
|
419
|
+
gbp_location_id=locations["locations"][0]["name"],
|
|
420
|
+
gbp_post_type="GALLERY",
|
|
421
|
+
gbp_media_category="EXTERIOR",
|
|
422
|
+
)
|
|
423
|
+
```
|
|
424
|
+
|
|
347
425
|
## Common Options
|
|
348
426
|
|
|
349
427
|
These options work across all upload methods:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|