upload-post 2.9.0__tar.gz → 2.12.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.9.0 → upload_post-2.12.0}/PKG-INFO +265 -3
- {upload_post-2.9.0 → upload_post-2.12.0}/README.md +264 -2
- {upload_post-2.9.0 → upload_post-2.12.0}/setup.py +1 -1
- upload_post-2.12.0/tests/test_comments_audience.py +231 -0
- {upload_post-2.9.0 → upload_post-2.12.0}/upload_post/__init__.py +1 -1
- {upload_post-2.9.0 → upload_post-2.12.0}/upload_post/api_client.py +425 -42
- {upload_post-2.9.0 → upload_post-2.12.0}/upload_post.egg-info/PKG-INFO +265 -3
- {upload_post-2.9.0 → upload_post-2.12.0}/upload_post.egg-info/SOURCES.txt +1 -0
- {upload_post-2.9.0 → upload_post-2.12.0}/setup.cfg +0 -0
- {upload_post-2.9.0 → upload_post-2.12.0}/upload_post/cli.py +0 -0
- {upload_post-2.9.0 → upload_post-2.12.0}/upload_post.egg-info/dependency_links.txt +0 -0
- {upload_post-2.9.0 → upload_post-2.12.0}/upload_post.egg-info/requires.txt +0 -0
- {upload_post-2.9.0 → upload_post-2.12.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.12.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
|
|
@@ -76,6 +76,8 @@ print(response)
|
|
|
76
76
|
- ✅ **Posting Queue** - Add posts to your configured queue
|
|
77
77
|
- ✅ **First Comments** - Auto-post first comment after publishing
|
|
78
78
|
- ✅ **Analytics** - Get engagement metrics
|
|
79
|
+
- ✅ **Audience** - Who follows a profile, per platform
|
|
80
|
+
- ✅ **Suggestions** - Hashtags and searches to post about, per platform
|
|
79
81
|
- ✅ **Full Type Hints**
|
|
80
82
|
|
|
81
83
|
## API Reference
|
|
@@ -97,7 +99,7 @@ response = client.upload_video(
|
|
|
97
99
|
first_comment="Thanks for watching! 🙏",
|
|
98
100
|
|
|
99
101
|
# Optional: Platform-specific settings
|
|
100
|
-
|
|
102
|
+
disable_comment=False, # TikTok
|
|
101
103
|
media_type="REELS", # Instagram
|
|
102
104
|
privacyStatus="public", # YouTube
|
|
103
105
|
tags=["tutorial", "coding"], # YouTube
|
|
@@ -309,12 +311,247 @@ li_pages = client.get_linkedin_pages("my-profile")
|
|
|
309
311
|
|
|
310
312
|
# Get Pinterest boards for a profile
|
|
311
313
|
boards = client.get_pinterest_boards("my-profile")
|
|
314
|
+
|
|
315
|
+
# TikTok: trending Commercial Music Library tracks
|
|
316
|
+
music = client.get_tiktok_trending_music(
|
|
317
|
+
"my-profile", genre="POP", country_code="ES", date_range="7DAY"
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
# TikTok: find a track by song or artist
|
|
321
|
+
found = client.search_tiktok_music("my-profile", q="bad bunny", country_code="ES")
|
|
322
|
+
|
|
323
|
+
# TikTok: search locations to tag
|
|
324
|
+
locations = client.get_tiktok_locations("my-profile", "Madrid")
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### Comments
|
|
328
|
+
|
|
329
|
+
The same methods cover every platform that has comments — Instagram, Facebook,
|
|
330
|
+
YouTube, LinkedIn and TikTok. There is no per-network method: the endpoint
|
|
331
|
+
answers one question and `platform` says who to ask.
|
|
332
|
+
|
|
333
|
+
```python
|
|
334
|
+
# Read the comments on a post. TikTok has no post-URL lookup: pass the video id.
|
|
335
|
+
comments = client.get_post_comments(
|
|
336
|
+
user="my-profile",
|
|
337
|
+
platform="tiktok",
|
|
338
|
+
post_id="7412345678901234567",
|
|
339
|
+
limit=20,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
# Read the replies hanging from one of them — same question, one more parameter
|
|
343
|
+
replies = client.get_post_comments(
|
|
344
|
+
user="my-profile",
|
|
345
|
+
platform="tiktok",
|
|
346
|
+
post_id="7412345678901234567",
|
|
347
|
+
comment_id="7412345678909999999",
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
# Comment on the post, or reply to a comment
|
|
351
|
+
client.create_comment(
|
|
352
|
+
user="my-profile",
|
|
353
|
+
platform="tiktok",
|
|
354
|
+
post_id="7412345678901234567",
|
|
355
|
+
message="Thanks for watching!",
|
|
356
|
+
)
|
|
357
|
+
client.create_comment(
|
|
358
|
+
user="my-profile",
|
|
359
|
+
platform="tiktok",
|
|
360
|
+
comment_id="7412345678909999999",
|
|
361
|
+
message="Glad you liked it",
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
# Delete one
|
|
365
|
+
client.delete_comment(
|
|
366
|
+
user="my-profile", platform="tiktok", comment_id="7412345678909999999"
|
|
367
|
+
)
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
You can also have the first comment posted for you right after publishing, with
|
|
371
|
+
`first_comment` for every platform or `tiktok_first_comment` for TikTok alone.
|
|
372
|
+
|
|
373
|
+
On TikTok all of this needs the `comments` capability, which the account grants
|
|
374
|
+
when it connects — see
|
|
375
|
+
[What a TikTok connection can do](#what-a-tiktok-connection-can-do-capabilities).
|
|
376
|
+
|
|
377
|
+
#### Moderating a comment: hide, like, pin
|
|
378
|
+
|
|
379
|
+
`comment_action()` does the three and undoes them, on any platform that
|
|
380
|
+
supports it. Each action carries its own inverse, and `post_id` is only sent
|
|
381
|
+
when the platform needs it:
|
|
382
|
+
|
|
383
|
+
```python
|
|
384
|
+
client.comment_action(
|
|
385
|
+
user="my-profile", platform="tiktok", action="hide",
|
|
386
|
+
comment_id="7412345678909999999", post_id="7412345678901234567",
|
|
387
|
+
)
|
|
388
|
+
client.comment_action(
|
|
389
|
+
user="my-profile", platform="tiktok", action="like",
|
|
390
|
+
comment_id="7412345678909999999",
|
|
391
|
+
)
|
|
392
|
+
client.comment_action(
|
|
393
|
+
user="my-profile", platform="tiktok", action="unpin",
|
|
394
|
+
comment_id="7412345678909999999", post_id="7412345678901234567",
|
|
395
|
+
)
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
| `action` | Undo | `post_id` |
|
|
399
|
+
| --- | --- | --- |
|
|
400
|
+
| `hide` | `unhide` | required |
|
|
401
|
+
| `like` | `unlike` | not sent |
|
|
402
|
+
| `pin` | `unpin` | required |
|
|
403
|
+
|
|
404
|
+
### Audience
|
|
405
|
+
|
|
406
|
+
Where the analytics methods answer *how did my posts do*, `get_audience()`
|
|
407
|
+
answers *who is following me*. One endpoint, one `platform` parameter, like
|
|
408
|
+
every other question in the API.
|
|
409
|
+
|
|
410
|
+
```python
|
|
411
|
+
audience = client.get_audience(
|
|
412
|
+
user="my-profile",
|
|
413
|
+
platform="tiktok",
|
|
414
|
+
start_date="2026-07-01",
|
|
415
|
+
end_date="2026-07-30",
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
print(audience["range"]) # the window actually used
|
|
419
|
+
print(audience["audience"]["countries"]) # and "cities", "ages", "genders"
|
|
420
|
+
print(audience["activity_by_hour"]) # [{"hour": "14", "followers_online": 1494}, ...]
|
|
421
|
+
print(audience["followers_daily"]) # [{"date", "total", "new", "lost"}, ...]
|
|
422
|
+
print(audience["profile_actions"]) # bio link, address, email, phone, leads
|
|
423
|
+
print(audience["bio_description"])
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
The window is clamped on the server: at most 60 days, and `end_date` always
|
|
427
|
+
before today. A wider window is trimmed to what the platform accepts instead of
|
|
428
|
+
failing.
|
|
429
|
+
|
|
430
|
+
Ask for a `benchmark_category` and the same call also returns how the account
|
|
431
|
+
compares with the average of that category. The accepted categories come back in
|
|
432
|
+
`benchmark_categories` on every response, so a picker needs no second call:
|
|
433
|
+
|
|
434
|
+
```python
|
|
435
|
+
categories = client.get_audience(
|
|
436
|
+
user="my-profile", platform="tiktok"
|
|
437
|
+
)["benchmark_categories"]
|
|
438
|
+
|
|
439
|
+
benchmark = client.get_audience(
|
|
440
|
+
user="my-profile", platform="tiktok",
|
|
441
|
+
benchmark_category="SOFTWARE_AND_APPS",
|
|
442
|
+
)["benchmark"]
|
|
443
|
+
print(benchmark["average_engagement_rate"], benchmark["average_video_views"])
|
|
312
444
|
```
|
|
313
445
|
|
|
446
|
+
### Suggestions
|
|
447
|
+
|
|
448
|
+
`get_suggestions()` answers *what is worth posting about*: the hashtags or the
|
|
449
|
+
searches a platform suggests around a keyword. One endpoint for both, told apart
|
|
450
|
+
by `type`.
|
|
451
|
+
|
|
452
|
+
```python
|
|
453
|
+
hashtags = client.get_suggestions(
|
|
454
|
+
user="my-profile", platform="tiktok", type="hashtags",
|
|
455
|
+
q="pilates", country_code="ES", language="es",
|
|
456
|
+
)["hashtags"]
|
|
457
|
+
print(hashtags) # [{"name": ..., "view_count": ...}, ...]
|
|
458
|
+
|
|
459
|
+
keywords = client.get_suggestions(
|
|
460
|
+
user="my-profile", platform="tiktok", type="keywords", q="pilates",
|
|
461
|
+
)["keywords"]
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
Per-post numbers stay in `get_post_analytics()`. On TikTok that response carries
|
|
465
|
+
more than the usual counters: `retention` (the curve, second by second),
|
|
466
|
+
`impression_sources` (For You, search, profile...), `audience_types` (followers
|
|
467
|
+
vs non-followers), `new_followers`, `reach` and the watch times.
|
|
468
|
+
|
|
469
|
+
Asking a platform a question it cannot answer fails with
|
|
470
|
+
`platform_not_supported` and the list of the ones that can.
|
|
471
|
+
|
|
472
|
+
## What a TikTok connection can do (`capabilities`)
|
|
473
|
+
|
|
474
|
+
Not every TikTok connection can do the same things. `client.list_users()`
|
|
475
|
+
(`GET /api/uploadposts/users`) returns a `capabilities` array on each profile's
|
|
476
|
+
TikTok account; check it before offering a feature.
|
|
477
|
+
|
|
478
|
+
| Capability | What it unlocks |
|
|
479
|
+
| --- | --- |
|
|
480
|
+
| `music` | `tiktok_music_id` and the volume/trim fields, plus `get_tiktok_trending_music()` and `search_tiktok_music()` |
|
|
481
|
+
| `location` | `tiktok_location_id` / `tiktok_location_name`, plus `get_tiktok_locations()` |
|
|
482
|
+
| `cover_image` | `tiktok_cover_image_url` |
|
|
483
|
+
| `cover_timestamp` | `cover_timestamp` |
|
|
484
|
+
| `draft` | `tiktok_upload_to_draft` |
|
|
485
|
+
| `video_privacy` | `privacy_level` on video |
|
|
486
|
+
| `photo_privacy` | `privacy_level` on photo posts |
|
|
487
|
+
| `profile_analytics` | `get_audience()` and `get_suggestions(type="hashtags")` with `platform="tiktok"` |
|
|
488
|
+
| `comments` | Comments on TikTok: `get_post_comments()` (top-level and replies), `create_comment()`, `delete_comment()`, `comment_action()` and `tiktok_first_comment` |
|
|
489
|
+
| `trend_search` | `get_suggestions(type="keywords")` with `platform="tiktok"` |
|
|
490
|
+
|
|
491
|
+
> **`comments` and `trend_search` need the account to be reconnected.** TikTok
|
|
492
|
+
> grants them at connect time, so an account linked before they existed keeps
|
|
493
|
+
> working for everything else but will not list them — reconnect it from Manage
|
|
494
|
+
> Users to enable them.
|
|
495
|
+
|
|
496
|
+
If a connection lacks a capability the upload field is simply ignored: the post
|
|
497
|
+
still publishes and the response carries a per-field `warnings` string. The
|
|
498
|
+
methods above answer with an error asking for a reconnection.
|
|
499
|
+
|
|
500
|
+
## TikTok music, location, cover and drafts
|
|
501
|
+
|
|
502
|
+
> Needs the `music`, `location`, `cover_image` or `draft` capability — see
|
|
503
|
+
> [What a TikTok connection can do](#what-a-tiktok-connection-can-do-capabilities).
|
|
504
|
+
|
|
505
|
+
```python
|
|
506
|
+
# 1. Pick a track and a place
|
|
507
|
+
tracks = client.get_tiktok_trending_music("my-profile", country_code="ES")["tracks"]
|
|
508
|
+
# ...or find one by name. TikTok has no music search endpoint, so this searches
|
|
509
|
+
# the trending charts Upload-Post caches, not TikTok's whole catalogue.
|
|
510
|
+
tracks = client.search_tiktok_music("my-profile", q="bossa", country_code="ES")["tracks"]
|
|
511
|
+
places = client.get_tiktok_locations("my-profile", "Madrid")["locations"]
|
|
512
|
+
|
|
513
|
+
# 2. Publish with them
|
|
514
|
+
client.upload_video(
|
|
515
|
+
"video.mp4",
|
|
516
|
+
title="Shot in Madrid",
|
|
517
|
+
user="my-profile",
|
|
518
|
+
platforms=["tiktok"],
|
|
519
|
+
|
|
520
|
+
tiktok_music_id=tracks[0]["id"],
|
|
521
|
+
tiktok_music_volume=70, # 0-100, defaults to 50 when music is set
|
|
522
|
+
tiktok_music_start=0, # ms
|
|
523
|
+
tiktok_music_end=15000, # ms
|
|
524
|
+
tiktok_original_sound_volume=30, # 0-100, defaults to 50 so the original audio is not muted
|
|
525
|
+
|
|
526
|
+
tiktok_location_id=places[0]["location_id"],
|
|
527
|
+
tiktok_location_name=places[0]["location_name"], # required together with the id
|
|
528
|
+
|
|
529
|
+
tiktok_cover_image_url="https://example.com/cover.jpg",
|
|
530
|
+
tiktok_is_ai_generated=False,
|
|
531
|
+
tiktok_upload_to_draft=False, # True sends it to drafts and ignores the rest
|
|
532
|
+
)
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
### TikTok music, location, cover and draft options
|
|
536
|
+
|
|
537
|
+
| Option | Type | Capability | Notes |
|
|
538
|
+
| --- | --- | --- | --- |
|
|
539
|
+
| `tiktok_music_id` | str | `music` | Video + photos. The track `id` from `get_tiktok_trending_music()` or `search_tiktok_music()` (not `commercial_music_id`) |
|
|
540
|
+
| `tiktok_music_volume` | int | `music` | Video only. 0-100, defaults to 50 when music is set |
|
|
541
|
+
| `tiktok_music_start` | int | `music` | Video only. Music start offset in ms |
|
|
542
|
+
| `tiktok_music_end` | int | `music` | Video only. Music end offset in ms |
|
|
543
|
+
| `tiktok_original_sound_volume` | int | `music` | Video only. 0-100, defaults to 50 when music is set, so the original audio is not muted |
|
|
544
|
+
| `tiktok_location_id` | str | `location` | Video + photos. `location_id` from `get_tiktok_locations()` |
|
|
545
|
+
| `tiktok_location_name` | str | `location` | Required whenever `tiktok_location_id` is set |
|
|
546
|
+
| `tiktok_cover_image_url` | str | `cover_image` | Video only. Custom cover image URL |
|
|
547
|
+
| `tiktok_is_ai_generated` | bool | — | Video + photos. AI-generated content disclosure |
|
|
548
|
+
| `tiktok_upload_to_draft` | bool | `draft` | Video only. Publish to drafts; TikTok ignores the rest of the post settings |
|
|
549
|
+
| `photo_cover_index` | int | — | Cover photo index for photo posts (0-based) |
|
|
550
|
+
| `privacy_level` | str | `photo_privacy` / `video_privacy` | Accepted on video and photo posts alike; which values the account may use is decided by TikTok, see the note below |
|
|
551
|
+
|
|
314
552
|
## Platform-Specific Options
|
|
315
553
|
|
|
316
554
|
### TikTok (Video)
|
|
317
|
-
- `privacy_level` - PUBLIC_TO_EVERYONE, MUTUAL_FOLLOW_FRIENDS, FOLLOWER_OF_CREATOR, SELF_ONLY
|
|
318
555
|
- `disable_duet` - Disable duet
|
|
319
556
|
- `disable_comment` - Disable comments
|
|
320
557
|
- `disable_stitch` - Disable stitch
|
|
@@ -324,10 +561,34 @@ boards = client.get_pinterest_boards("my-profile")
|
|
|
324
561
|
- `brand_content_toggle` - Branded content toggle
|
|
325
562
|
- `brand_organic_toggle` - Brand organic toggle
|
|
326
563
|
|
|
564
|
+
> **TikTok particularity:** `privacy_level` works on video and photo posts
|
|
565
|
+
> alike, but **TikTok decides per account which values are available**. A
|
|
566
|
+
> private account, for example, is offered `FOLLOWER_OF_CREATOR`,
|
|
567
|
+
> `MUTUAL_FOLLOW_FRIENDS` and `SELF_ONLY`, with no `PUBLIC_TO_EVERYONE`; asking
|
|
568
|
+
> for one the account does not have fails with
|
|
569
|
+
> `error_code="tiktok_privacy_unavailable"` and an error listing the ones it
|
|
570
|
+
> does have. Omit it on video and TikTok applies the account's own default; on
|
|
571
|
+
> photo posts it defaults to `PUBLIC_TO_EVERYONE`. To offer only the values that
|
|
572
|
+
> will actually work, ask the account with
|
|
573
|
+
> `client.get_tiktok_publishing_settings(profile)` and read
|
|
574
|
+
> `privacy_level_options`.
|
|
575
|
+
|
|
576
|
+
See [TikTok music, location, cover and drafts](#tiktok-music-location-cover-and-drafts)
|
|
577
|
+
for the music, location, cover and draft options.
|
|
578
|
+
|
|
327
579
|
### TikTok (Photos)
|
|
580
|
+
- `privacy_level` - PUBLIC_TO_EVERYONE, MUTUAL_FOLLOW_FRIENDS, FOLLOWER_OF_CREATOR, SELF_ONLY
|
|
581
|
+
- `post_mode` - DIRECT_POST or MEDIA_UPLOAD
|
|
328
582
|
- `auto_add_music` - Auto add music
|
|
329
583
|
- `photo_cover_index` - Index of photo for cover (0-based)
|
|
330
584
|
- `disable_comment` - Disable comments
|
|
585
|
+
- `tiktok_music_id` - Commercial Music Library track id
|
|
586
|
+
- `tiktok_location_id` / `tiktok_location_name` - Location tag, both required together
|
|
587
|
+
- `tiktok_is_ai_generated` - AI-generated content disclosure
|
|
588
|
+
|
|
589
|
+
> TikTok's photo contract takes the music track id alone: `tiktok_music_volume`,
|
|
590
|
+
> `tiktok_music_start`, `tiktok_music_end`, `tiktok_original_sound_volume`,
|
|
591
|
+
> `tiktok_cover_image_url` and `tiktok_upload_to_draft` are video-only.
|
|
331
592
|
|
|
332
593
|
### Instagram
|
|
333
594
|
- `media_type` - REELS, STORIES, IMAGE
|
|
@@ -432,6 +693,7 @@ These options work across all upload methods:
|
|
|
432
693
|
| `user` | Profile name (required) |
|
|
433
694
|
| `platforms` | Target platforms list (required) |
|
|
434
695
|
| `first_comment` | First comment to post |
|
|
696
|
+
| `tiktok_first_comment` | First comment for TikTok only (needs the `comments` capability) |
|
|
435
697
|
| `alt_text` | Alt text for accessibility |
|
|
436
698
|
| `scheduled_date` | ISO date for scheduling |
|
|
437
699
|
| `timezone` | Timezone for scheduled date |
|
|
@@ -38,6 +38,8 @@ print(response)
|
|
|
38
38
|
- ✅ **Posting Queue** - Add posts to your configured queue
|
|
39
39
|
- ✅ **First Comments** - Auto-post first comment after publishing
|
|
40
40
|
- ✅ **Analytics** - Get engagement metrics
|
|
41
|
+
- ✅ **Audience** - Who follows a profile, per platform
|
|
42
|
+
- ✅ **Suggestions** - Hashtags and searches to post about, per platform
|
|
41
43
|
- ✅ **Full Type Hints**
|
|
42
44
|
|
|
43
45
|
## API Reference
|
|
@@ -59,7 +61,7 @@ response = client.upload_video(
|
|
|
59
61
|
first_comment="Thanks for watching! 🙏",
|
|
60
62
|
|
|
61
63
|
# Optional: Platform-specific settings
|
|
62
|
-
|
|
64
|
+
disable_comment=False, # TikTok
|
|
63
65
|
media_type="REELS", # Instagram
|
|
64
66
|
privacyStatus="public", # YouTube
|
|
65
67
|
tags=["tutorial", "coding"], # YouTube
|
|
@@ -271,12 +273,247 @@ li_pages = client.get_linkedin_pages("my-profile")
|
|
|
271
273
|
|
|
272
274
|
# Get Pinterest boards for a profile
|
|
273
275
|
boards = client.get_pinterest_boards("my-profile")
|
|
276
|
+
|
|
277
|
+
# TikTok: trending Commercial Music Library tracks
|
|
278
|
+
music = client.get_tiktok_trending_music(
|
|
279
|
+
"my-profile", genre="POP", country_code="ES", date_range="7DAY"
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
# TikTok: find a track by song or artist
|
|
283
|
+
found = client.search_tiktok_music("my-profile", q="bad bunny", country_code="ES")
|
|
284
|
+
|
|
285
|
+
# TikTok: search locations to tag
|
|
286
|
+
locations = client.get_tiktok_locations("my-profile", "Madrid")
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### Comments
|
|
290
|
+
|
|
291
|
+
The same methods cover every platform that has comments — Instagram, Facebook,
|
|
292
|
+
YouTube, LinkedIn and TikTok. There is no per-network method: the endpoint
|
|
293
|
+
answers one question and `platform` says who to ask.
|
|
294
|
+
|
|
295
|
+
```python
|
|
296
|
+
# Read the comments on a post. TikTok has no post-URL lookup: pass the video id.
|
|
297
|
+
comments = client.get_post_comments(
|
|
298
|
+
user="my-profile",
|
|
299
|
+
platform="tiktok",
|
|
300
|
+
post_id="7412345678901234567",
|
|
301
|
+
limit=20,
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
# Read the replies hanging from one of them — same question, one more parameter
|
|
305
|
+
replies = client.get_post_comments(
|
|
306
|
+
user="my-profile",
|
|
307
|
+
platform="tiktok",
|
|
308
|
+
post_id="7412345678901234567",
|
|
309
|
+
comment_id="7412345678909999999",
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
# Comment on the post, or reply to a comment
|
|
313
|
+
client.create_comment(
|
|
314
|
+
user="my-profile",
|
|
315
|
+
platform="tiktok",
|
|
316
|
+
post_id="7412345678901234567",
|
|
317
|
+
message="Thanks for watching!",
|
|
318
|
+
)
|
|
319
|
+
client.create_comment(
|
|
320
|
+
user="my-profile",
|
|
321
|
+
platform="tiktok",
|
|
322
|
+
comment_id="7412345678909999999",
|
|
323
|
+
message="Glad you liked it",
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
# Delete one
|
|
327
|
+
client.delete_comment(
|
|
328
|
+
user="my-profile", platform="tiktok", comment_id="7412345678909999999"
|
|
329
|
+
)
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
You can also have the first comment posted for you right after publishing, with
|
|
333
|
+
`first_comment` for every platform or `tiktok_first_comment` for TikTok alone.
|
|
334
|
+
|
|
335
|
+
On TikTok all of this needs the `comments` capability, which the account grants
|
|
336
|
+
when it connects — see
|
|
337
|
+
[What a TikTok connection can do](#what-a-tiktok-connection-can-do-capabilities).
|
|
338
|
+
|
|
339
|
+
#### Moderating a comment: hide, like, pin
|
|
340
|
+
|
|
341
|
+
`comment_action()` does the three and undoes them, on any platform that
|
|
342
|
+
supports it. Each action carries its own inverse, and `post_id` is only sent
|
|
343
|
+
when the platform needs it:
|
|
344
|
+
|
|
345
|
+
```python
|
|
346
|
+
client.comment_action(
|
|
347
|
+
user="my-profile", platform="tiktok", action="hide",
|
|
348
|
+
comment_id="7412345678909999999", post_id="7412345678901234567",
|
|
349
|
+
)
|
|
350
|
+
client.comment_action(
|
|
351
|
+
user="my-profile", platform="tiktok", action="like",
|
|
352
|
+
comment_id="7412345678909999999",
|
|
353
|
+
)
|
|
354
|
+
client.comment_action(
|
|
355
|
+
user="my-profile", platform="tiktok", action="unpin",
|
|
356
|
+
comment_id="7412345678909999999", post_id="7412345678901234567",
|
|
357
|
+
)
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
| `action` | Undo | `post_id` |
|
|
361
|
+
| --- | --- | --- |
|
|
362
|
+
| `hide` | `unhide` | required |
|
|
363
|
+
| `like` | `unlike` | not sent |
|
|
364
|
+
| `pin` | `unpin` | required |
|
|
365
|
+
|
|
366
|
+
### Audience
|
|
367
|
+
|
|
368
|
+
Where the analytics methods answer *how did my posts do*, `get_audience()`
|
|
369
|
+
answers *who is following me*. One endpoint, one `platform` parameter, like
|
|
370
|
+
every other question in the API.
|
|
371
|
+
|
|
372
|
+
```python
|
|
373
|
+
audience = client.get_audience(
|
|
374
|
+
user="my-profile",
|
|
375
|
+
platform="tiktok",
|
|
376
|
+
start_date="2026-07-01",
|
|
377
|
+
end_date="2026-07-30",
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
print(audience["range"]) # the window actually used
|
|
381
|
+
print(audience["audience"]["countries"]) # and "cities", "ages", "genders"
|
|
382
|
+
print(audience["activity_by_hour"]) # [{"hour": "14", "followers_online": 1494}, ...]
|
|
383
|
+
print(audience["followers_daily"]) # [{"date", "total", "new", "lost"}, ...]
|
|
384
|
+
print(audience["profile_actions"]) # bio link, address, email, phone, leads
|
|
385
|
+
print(audience["bio_description"])
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
The window is clamped on the server: at most 60 days, and `end_date` always
|
|
389
|
+
before today. A wider window is trimmed to what the platform accepts instead of
|
|
390
|
+
failing.
|
|
391
|
+
|
|
392
|
+
Ask for a `benchmark_category` and the same call also returns how the account
|
|
393
|
+
compares with the average of that category. The accepted categories come back in
|
|
394
|
+
`benchmark_categories` on every response, so a picker needs no second call:
|
|
395
|
+
|
|
396
|
+
```python
|
|
397
|
+
categories = client.get_audience(
|
|
398
|
+
user="my-profile", platform="tiktok"
|
|
399
|
+
)["benchmark_categories"]
|
|
400
|
+
|
|
401
|
+
benchmark = client.get_audience(
|
|
402
|
+
user="my-profile", platform="tiktok",
|
|
403
|
+
benchmark_category="SOFTWARE_AND_APPS",
|
|
404
|
+
)["benchmark"]
|
|
405
|
+
print(benchmark["average_engagement_rate"], benchmark["average_video_views"])
|
|
274
406
|
```
|
|
275
407
|
|
|
408
|
+
### Suggestions
|
|
409
|
+
|
|
410
|
+
`get_suggestions()` answers *what is worth posting about*: the hashtags or the
|
|
411
|
+
searches a platform suggests around a keyword. One endpoint for both, told apart
|
|
412
|
+
by `type`.
|
|
413
|
+
|
|
414
|
+
```python
|
|
415
|
+
hashtags = client.get_suggestions(
|
|
416
|
+
user="my-profile", platform="tiktok", type="hashtags",
|
|
417
|
+
q="pilates", country_code="ES", language="es",
|
|
418
|
+
)["hashtags"]
|
|
419
|
+
print(hashtags) # [{"name": ..., "view_count": ...}, ...]
|
|
420
|
+
|
|
421
|
+
keywords = client.get_suggestions(
|
|
422
|
+
user="my-profile", platform="tiktok", type="keywords", q="pilates",
|
|
423
|
+
)["keywords"]
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
Per-post numbers stay in `get_post_analytics()`. On TikTok that response carries
|
|
427
|
+
more than the usual counters: `retention` (the curve, second by second),
|
|
428
|
+
`impression_sources` (For You, search, profile...), `audience_types` (followers
|
|
429
|
+
vs non-followers), `new_followers`, `reach` and the watch times.
|
|
430
|
+
|
|
431
|
+
Asking a platform a question it cannot answer fails with
|
|
432
|
+
`platform_not_supported` and the list of the ones that can.
|
|
433
|
+
|
|
434
|
+
## What a TikTok connection can do (`capabilities`)
|
|
435
|
+
|
|
436
|
+
Not every TikTok connection can do the same things. `client.list_users()`
|
|
437
|
+
(`GET /api/uploadposts/users`) returns a `capabilities` array on each profile's
|
|
438
|
+
TikTok account; check it before offering a feature.
|
|
439
|
+
|
|
440
|
+
| Capability | What it unlocks |
|
|
441
|
+
| --- | --- |
|
|
442
|
+
| `music` | `tiktok_music_id` and the volume/trim fields, plus `get_tiktok_trending_music()` and `search_tiktok_music()` |
|
|
443
|
+
| `location` | `tiktok_location_id` / `tiktok_location_name`, plus `get_tiktok_locations()` |
|
|
444
|
+
| `cover_image` | `tiktok_cover_image_url` |
|
|
445
|
+
| `cover_timestamp` | `cover_timestamp` |
|
|
446
|
+
| `draft` | `tiktok_upload_to_draft` |
|
|
447
|
+
| `video_privacy` | `privacy_level` on video |
|
|
448
|
+
| `photo_privacy` | `privacy_level` on photo posts |
|
|
449
|
+
| `profile_analytics` | `get_audience()` and `get_suggestions(type="hashtags")` with `platform="tiktok"` |
|
|
450
|
+
| `comments` | Comments on TikTok: `get_post_comments()` (top-level and replies), `create_comment()`, `delete_comment()`, `comment_action()` and `tiktok_first_comment` |
|
|
451
|
+
| `trend_search` | `get_suggestions(type="keywords")` with `platform="tiktok"` |
|
|
452
|
+
|
|
453
|
+
> **`comments` and `trend_search` need the account to be reconnected.** TikTok
|
|
454
|
+
> grants them at connect time, so an account linked before they existed keeps
|
|
455
|
+
> working for everything else but will not list them — reconnect it from Manage
|
|
456
|
+
> Users to enable them.
|
|
457
|
+
|
|
458
|
+
If a connection lacks a capability the upload field is simply ignored: the post
|
|
459
|
+
still publishes and the response carries a per-field `warnings` string. The
|
|
460
|
+
methods above answer with an error asking for a reconnection.
|
|
461
|
+
|
|
462
|
+
## TikTok music, location, cover and drafts
|
|
463
|
+
|
|
464
|
+
> Needs the `music`, `location`, `cover_image` or `draft` capability — see
|
|
465
|
+
> [What a TikTok connection can do](#what-a-tiktok-connection-can-do-capabilities).
|
|
466
|
+
|
|
467
|
+
```python
|
|
468
|
+
# 1. Pick a track and a place
|
|
469
|
+
tracks = client.get_tiktok_trending_music("my-profile", country_code="ES")["tracks"]
|
|
470
|
+
# ...or find one by name. TikTok has no music search endpoint, so this searches
|
|
471
|
+
# the trending charts Upload-Post caches, not TikTok's whole catalogue.
|
|
472
|
+
tracks = client.search_tiktok_music("my-profile", q="bossa", country_code="ES")["tracks"]
|
|
473
|
+
places = client.get_tiktok_locations("my-profile", "Madrid")["locations"]
|
|
474
|
+
|
|
475
|
+
# 2. Publish with them
|
|
476
|
+
client.upload_video(
|
|
477
|
+
"video.mp4",
|
|
478
|
+
title="Shot in Madrid",
|
|
479
|
+
user="my-profile",
|
|
480
|
+
platforms=["tiktok"],
|
|
481
|
+
|
|
482
|
+
tiktok_music_id=tracks[0]["id"],
|
|
483
|
+
tiktok_music_volume=70, # 0-100, defaults to 50 when music is set
|
|
484
|
+
tiktok_music_start=0, # ms
|
|
485
|
+
tiktok_music_end=15000, # ms
|
|
486
|
+
tiktok_original_sound_volume=30, # 0-100, defaults to 50 so the original audio is not muted
|
|
487
|
+
|
|
488
|
+
tiktok_location_id=places[0]["location_id"],
|
|
489
|
+
tiktok_location_name=places[0]["location_name"], # required together with the id
|
|
490
|
+
|
|
491
|
+
tiktok_cover_image_url="https://example.com/cover.jpg",
|
|
492
|
+
tiktok_is_ai_generated=False,
|
|
493
|
+
tiktok_upload_to_draft=False, # True sends it to drafts and ignores the rest
|
|
494
|
+
)
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
### TikTok music, location, cover and draft options
|
|
498
|
+
|
|
499
|
+
| Option | Type | Capability | Notes |
|
|
500
|
+
| --- | --- | --- | --- |
|
|
501
|
+
| `tiktok_music_id` | str | `music` | Video + photos. The track `id` from `get_tiktok_trending_music()` or `search_tiktok_music()` (not `commercial_music_id`) |
|
|
502
|
+
| `tiktok_music_volume` | int | `music` | Video only. 0-100, defaults to 50 when music is set |
|
|
503
|
+
| `tiktok_music_start` | int | `music` | Video only. Music start offset in ms |
|
|
504
|
+
| `tiktok_music_end` | int | `music` | Video only. Music end offset in ms |
|
|
505
|
+
| `tiktok_original_sound_volume` | int | `music` | Video only. 0-100, defaults to 50 when music is set, so the original audio is not muted |
|
|
506
|
+
| `tiktok_location_id` | str | `location` | Video + photos. `location_id` from `get_tiktok_locations()` |
|
|
507
|
+
| `tiktok_location_name` | str | `location` | Required whenever `tiktok_location_id` is set |
|
|
508
|
+
| `tiktok_cover_image_url` | str | `cover_image` | Video only. Custom cover image URL |
|
|
509
|
+
| `tiktok_is_ai_generated` | bool | — | Video + photos. AI-generated content disclosure |
|
|
510
|
+
| `tiktok_upload_to_draft` | bool | `draft` | Video only. Publish to drafts; TikTok ignores the rest of the post settings |
|
|
511
|
+
| `photo_cover_index` | int | — | Cover photo index for photo posts (0-based) |
|
|
512
|
+
| `privacy_level` | str | `photo_privacy` / `video_privacy` | Accepted on video and photo posts alike; which values the account may use is decided by TikTok, see the note below |
|
|
513
|
+
|
|
276
514
|
## Platform-Specific Options
|
|
277
515
|
|
|
278
516
|
### TikTok (Video)
|
|
279
|
-
- `privacy_level` - PUBLIC_TO_EVERYONE, MUTUAL_FOLLOW_FRIENDS, FOLLOWER_OF_CREATOR, SELF_ONLY
|
|
280
517
|
- `disable_duet` - Disable duet
|
|
281
518
|
- `disable_comment` - Disable comments
|
|
282
519
|
- `disable_stitch` - Disable stitch
|
|
@@ -286,10 +523,34 @@ boards = client.get_pinterest_boards("my-profile")
|
|
|
286
523
|
- `brand_content_toggle` - Branded content toggle
|
|
287
524
|
- `brand_organic_toggle` - Brand organic toggle
|
|
288
525
|
|
|
526
|
+
> **TikTok particularity:** `privacy_level` works on video and photo posts
|
|
527
|
+
> alike, but **TikTok decides per account which values are available**. A
|
|
528
|
+
> private account, for example, is offered `FOLLOWER_OF_CREATOR`,
|
|
529
|
+
> `MUTUAL_FOLLOW_FRIENDS` and `SELF_ONLY`, with no `PUBLIC_TO_EVERYONE`; asking
|
|
530
|
+
> for one the account does not have fails with
|
|
531
|
+
> `error_code="tiktok_privacy_unavailable"` and an error listing the ones it
|
|
532
|
+
> does have. Omit it on video and TikTok applies the account's own default; on
|
|
533
|
+
> photo posts it defaults to `PUBLIC_TO_EVERYONE`. To offer only the values that
|
|
534
|
+
> will actually work, ask the account with
|
|
535
|
+
> `client.get_tiktok_publishing_settings(profile)` and read
|
|
536
|
+
> `privacy_level_options`.
|
|
537
|
+
|
|
538
|
+
See [TikTok music, location, cover and drafts](#tiktok-music-location-cover-and-drafts)
|
|
539
|
+
for the music, location, cover and draft options.
|
|
540
|
+
|
|
289
541
|
### TikTok (Photos)
|
|
542
|
+
- `privacy_level` - PUBLIC_TO_EVERYONE, MUTUAL_FOLLOW_FRIENDS, FOLLOWER_OF_CREATOR, SELF_ONLY
|
|
543
|
+
- `post_mode` - DIRECT_POST or MEDIA_UPLOAD
|
|
290
544
|
- `auto_add_music` - Auto add music
|
|
291
545
|
- `photo_cover_index` - Index of photo for cover (0-based)
|
|
292
546
|
- `disable_comment` - Disable comments
|
|
547
|
+
- `tiktok_music_id` - Commercial Music Library track id
|
|
548
|
+
- `tiktok_location_id` / `tiktok_location_name` - Location tag, both required together
|
|
549
|
+
- `tiktok_is_ai_generated` - AI-generated content disclosure
|
|
550
|
+
|
|
551
|
+
> TikTok's photo contract takes the music track id alone: `tiktok_music_volume`,
|
|
552
|
+
> `tiktok_music_start`, `tiktok_music_end`, `tiktok_original_sound_volume`,
|
|
553
|
+
> `tiktok_cover_image_url` and `tiktok_upload_to_draft` are video-only.
|
|
293
554
|
|
|
294
555
|
### Instagram
|
|
295
556
|
- `media_type` - REELS, STORIES, IMAGE
|
|
@@ -394,6 +655,7 @@ These options work across all upload methods:
|
|
|
394
655
|
| `user` | Profile name (required) |
|
|
395
656
|
| `platforms` | Target platforms list (required) |
|
|
396
657
|
| `first_comment` | First comment to post |
|
|
658
|
+
| `tiktok_first_comment` | First comment for TikTok only (needs the `comments` capability) |
|
|
397
659
|
| `alt_text` | Alt text for accessibility |
|
|
398
660
|
| `scheduled_date` | ISO date for scheduling |
|
|
399
661
|
| `timezone` | Timezone for scheduled date |
|
|
@@ -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.12.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)",
|