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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: upload-post
3
- Version: 2.9.0
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
- privacy_level="PUBLIC_TO_EVERYONE", # TikTok
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
- privacy_level="PUBLIC_TO_EVERYONE", # TikTok
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.9.0",
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)",