upload-post 2.1.0__tar.gz → 2.2.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.1.0
3
+ Version: 2.2.0
4
4
  Summary: Cross-platform social media upload for TikTok, Instagram, YouTube, LinkedIn, Facebook, Pinterest, Threads, Reddit, Bluesky, and X (Twitter)
5
5
  Home-page: https://www.upload-post.com/
6
6
  Author: Upload-Post
@@ -223,6 +223,23 @@ analytics = client.get_analytics(
223
223
  print(analytics)
224
224
  ```
225
225
 
226
+ ### Get Media
227
+
228
+ Retrieve recent posts from a connected social account. Supported platforms:
229
+ `instagram`, `tiktok`, `youtube`, `linkedin`, `facebook`, `x`, `threads`,
230
+ `pinterest`, `bluesky`, `reddit`.
231
+
232
+ ```python
233
+ # Personal LinkedIn profile (default for non-org accounts):
234
+ media = client.get_media("linkedin", "my-profile")
235
+
236
+ # Force the personal profile of an account connected as an org admin:
237
+ media = client.get_media("linkedin", "my-profile", page_urn="me")
238
+
239
+ # Target a specific LinkedIn organization page:
240
+ media = client.get_media("linkedin", "my-profile", page_urn="12345")
241
+ ```
242
+
226
243
  ### Helper Methods
227
244
 
228
245
  ```python
@@ -185,6 +185,23 @@ analytics = client.get_analytics(
185
185
  print(analytics)
186
186
  ```
187
187
 
188
+ ### Get Media
189
+
190
+ Retrieve recent posts from a connected social account. Supported platforms:
191
+ `instagram`, `tiktok`, `youtube`, `linkedin`, `facebook`, `x`, `threads`,
192
+ `pinterest`, `bluesky`, `reddit`.
193
+
194
+ ```python
195
+ # Personal LinkedIn profile (default for non-org accounts):
196
+ media = client.get_media("linkedin", "my-profile")
197
+
198
+ # Force the personal profile of an account connected as an org admin:
199
+ media = client.get_media("linkedin", "my-profile", page_urn="me")
200
+
201
+ # Target a specific LinkedIn organization page:
202
+ media = client.get_media("linkedin", "my-profile", page_urn="12345")
203
+ ```
204
+
188
205
  ### Helper Methods
189
206
 
190
207
  ```python
@@ -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.1.0",
8
+ version="2.2.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, and X (Twitter)",
@@ -15,7 +15,7 @@ Example:
15
15
  ... )
16
16
  """
17
17
 
18
- __version__ = "2.1.0"
18
+ __version__ = "2.2.0"
19
19
 
20
20
  from .api_client import UploadPostClient, UploadPostError
21
21
 
@@ -183,7 +183,7 @@ class UploadPostClient:
183
183
  if kwargs.get("photo_cover_index") is not None:
184
184
  data.append(("photo_cover_index", str(kwargs["photo_cover_index"])))
185
185
 
186
- def _add_instagram_params(self, data: List[tuple], is_video: bool = True, **kwargs):
186
+ def _add_instagram_params(self, data: List[tuple], is_video: bool = True, files: List[tuple] | None = None, **kwargs):
187
187
  """Add Instagram-specific parameters."""
188
188
  if kwargs.get("media_type"):
189
189
  data.append(("media_type", kwargs["media_type"]))
@@ -193,18 +193,26 @@ class UploadPostClient:
193
193
  data.append(("user_tags", kwargs["user_tags"]))
194
194
  if kwargs.get("location_id"):
195
195
  data.append(("location_id", kwargs["location_id"]))
196
-
196
+
197
197
  if is_video:
198
198
  if kwargs.get("share_to_feed") is not None:
199
199
  data.append(("share_to_feed", str(kwargs["share_to_feed"]).lower()))
200
200
  if kwargs.get("cover_url"):
201
- data.append(("cover_url", kwargs["cover_url"]))
201
+ cover_val = str(kwargs["cover_url"])
202
+ if cover_val.lower().startswith(("http://", "https://")):
203
+ data.append(("cover_url", cover_val))
204
+ elif files is not None:
205
+ cover_path = Path(cover_val)
206
+ if cover_path.exists():
207
+ files.append(("cover_image", (cover_path.name, cover_path.open("rb"))))
208
+ else:
209
+ data.append(("cover_url", cover_val))
202
210
  if kwargs.get("audio_name"):
203
211
  data.append(("audio_name", kwargs["audio_name"]))
204
212
  if kwargs.get("thumb_offset"):
205
213
  data.append(("thumb_offset", kwargs["thumb_offset"]))
206
214
 
207
- def _add_youtube_params(self, data: List[tuple], **kwargs):
215
+ def _add_youtube_params(self, data: List[tuple], files: List[tuple] = None, **kwargs):
208
216
  """Add YouTube-specific parameters."""
209
217
  if kwargs.get("tags"):
210
218
  tags = kwargs["tags"]
@@ -240,6 +248,21 @@ class UploadPostClient:
240
248
  data.append(("hasPaidProductPlacement", str(kwargs["hasPaidProductPlacement"]).lower()))
241
249
  if kwargs.get("recordingDate"):
242
250
  data.append(("recordingDate", kwargs["recordingDate"]))
251
+ if kwargs.get("subtitles"):
252
+ for idx, sub in enumerate(kwargs["subtitles"]):
253
+ if sub.get("language"):
254
+ data.append((f"youtube_subtitle_language_{idx}", sub["language"]))
255
+ if sub.get("name"):
256
+ data.append((f"youtube_subtitle_name_{idx}", sub["name"]))
257
+ if sub.get("file"):
258
+ sub_path = Path(sub["file"])
259
+ if sub_path.exists() and files is not None:
260
+ files.append((f"youtube_subtitle_file_{idx}", (sub_path.name, sub_path.open("rb"))))
261
+ else:
262
+ # Treat as URL string
263
+ data.append((f"youtube_subtitle_file_{idx}", str(sub["file"])))
264
+ elif sub.get("url"):
265
+ data.append((f"youtube_subtitle_file_{idx}", sub["url"]))
243
266
 
244
267
  def _add_linkedin_params(self, data: List[tuple], is_text: bool = False, **kwargs):
245
268
  """Add LinkedIn-specific parameters."""
@@ -399,7 +422,7 @@ class UploadPostClient:
399
422
  media_type: REELS or STORIES
400
423
  share_to_feed: Share to feed
401
424
  collaborators: Comma-separated collaborator usernames
402
- cover_url: Custom cover URL
425
+ cover_url: Custom cover URL or file path. URLs are sent directly; file paths are uploaded as binary.
403
426
  audio_name: Audio track name
404
427
  user_tags: Comma-separated user tags
405
428
  location_id: Location ID
@@ -421,7 +444,9 @@ class UploadPostClient:
421
444
  blockedCountries: Comma-separated country codes
422
445
  hasPaidProductPlacement: Paid placement flag
423
446
  recordingDate: Recording date (ISO 8601)
424
-
447
+ subtitles: List of subtitle dicts with keys: language (BCP-47),
448
+ name (display name), file (path to SRT/VTT file), url (subtitle URL)
449
+
425
450
  LinkedIn:
426
451
  visibility: PUBLIC, CONNECTIONS, LOGGED_IN, CONTAINER
427
452
  target_linkedin_page_id: Page ID for organization posts
@@ -479,9 +504,9 @@ class UploadPostClient:
479
504
  if "tiktok" in platforms:
480
505
  self._add_tiktok_params(data, is_video=True, **kwargs)
481
506
  if "instagram" in platforms:
482
- self._add_instagram_params(data, is_video=True, **kwargs)
507
+ self._add_instagram_params(data, is_video=True, files=files, **kwargs)
483
508
  if "youtube" in platforms:
484
- self._add_youtube_params(data, **kwargs)
509
+ self._add_youtube_params(data, files=files, **kwargs)
485
510
  if "linkedin" in platforms:
486
511
  self._add_linkedin_params(data, **kwargs)
487
512
  if "facebook" in platforms:
@@ -937,6 +962,25 @@ class UploadPostClient:
937
962
  """
938
963
  return self._request(f"/uploadposts/post-analytics/{request_id}", "GET")
939
964
 
965
+ def get_post_analytics_by_platform_id(self, platform_post_id: str, platform: str, user: str) -> Dict:
966
+ """
967
+ Get analytics for any post (including organic posts) using its native platform ID.
968
+
969
+ Args:
970
+ platform_post_id: The native post ID on the platform (e.g., Instagram media ID).
971
+ platform: The platform to query (instagram, youtube, tiktok, facebook, linkedin, x, threads, pinterest, reddit).
972
+ user: The profile_username that owns the social account.
973
+
974
+ Returns:
975
+ Post analytics with live per-post metrics from the platform API.
976
+ """
977
+ params = {
978
+ "platform_post_id": platform_post_id,
979
+ "platform": platform,
980
+ "user": user,
981
+ }
982
+ return self._request("/uploadposts/post-analytics", "GET", params=params)
983
+
940
984
  def get_platform_metrics(self) -> Dict:
941
985
  """
942
986
  Get available metrics configuration for all supported platforms.
@@ -946,6 +990,30 @@ class UploadPostClient:
946
990
  """
947
991
  return self._request("/uploadposts/platform-metrics", "GET")
948
992
 
993
+ def get_media(
994
+ self,
995
+ platform: str,
996
+ user: str,
997
+ page_urn: Optional[str] = None,
998
+ ) -> Dict:
999
+ """
1000
+ Retrieve recent media from a connected social account.
1001
+
1002
+ Args:
1003
+ platform: instagram, tiktok, youtube, linkedin, facebook, x,
1004
+ threads, pinterest, bluesky, or reddit.
1005
+ user: Profile username.
1006
+ page_urn: LinkedIn only. Numeric org ID, full URN, or ``"me"`` to
1007
+ force the personal profile of an org-admin account.
1008
+
1009
+ Returns:
1010
+ ``{"success": True, "media": [...]}``.
1011
+ """
1012
+ params: Dict[str, str] = {"platform": platform, "user": user}
1013
+ if page_urn:
1014
+ params["page_urn"] = page_urn
1015
+ return self._request("/uploadposts/media", "GET", params=params)
1016
+
949
1017
  # ==================== Scheduled Posts ====================
950
1018
 
951
1019
  def list_scheduled(self) -> Dict:
@@ -1089,6 +1157,66 @@ class UploadPostClient:
1089
1157
  """
1090
1158
  return self._request("/uploadposts/users/validate-jwt", "POST", json_data={"jwt": jwt})
1091
1159
 
1160
+ def get_user_preferences(self) -> Dict:
1161
+ """
1162
+ Get user preferences (including calendar settings).
1163
+
1164
+ Returns:
1165
+ User preferences including week_start_day.
1166
+ """
1167
+ return self._request("/uploadposts/users/preferences", "GET")
1168
+
1169
+ def update_user_preferences(
1170
+ self,
1171
+ week_start_day: Optional[int] = None
1172
+ ) -> Dict:
1173
+ """
1174
+ Update user preferences (including calendar settings).
1175
+
1176
+ Args:
1177
+ week_start_day: Week start day (0=Sunday, 1=Monday).
1178
+
1179
+ Returns:
1180
+ Updated preferences.
1181
+ """
1182
+ body: Dict[str, Any] = {}
1183
+ if week_start_day is not None:
1184
+ body["week_start_day"] = week_start_day
1185
+ return self._request("/uploadposts/users/preferences", "POST", json_data=body)
1186
+
1187
+ def get_notification_config(self) -> Dict:
1188
+ """
1189
+ Get notification configuration (including webhook settings).
1190
+
1191
+ Returns:
1192
+ Notification config including webhook_events and webhook_url.
1193
+ """
1194
+ return self._request("/uploadposts/notification-config", "GET")
1195
+
1196
+ def update_notification_config(
1197
+ self,
1198
+ webhook_events: Optional[List[str]] = None,
1199
+ webhook_url: Optional[str] = None
1200
+ ) -> Dict:
1201
+ """
1202
+ Update notification configuration (including webhook settings).
1203
+
1204
+ Args:
1205
+ webhook_events: Webhook event types to subscribe to
1206
+ (upload_completed, social_account_connected,
1207
+ social_account_disconnected, social_account_reauth_required).
1208
+ webhook_url: Webhook URL for notifications.
1209
+
1210
+ Returns:
1211
+ Updated notification config.
1212
+ """
1213
+ body: Dict[str, Any] = {}
1214
+ if webhook_events:
1215
+ body["webhook_events"] = webhook_events
1216
+ if webhook_url:
1217
+ body["webhook_url"] = webhook_url
1218
+ return self._request("/uploadposts/notification-config", "POST", json_data=body)
1219
+
1092
1220
  # ==================== Helper Endpoints ====================
1093
1221
 
1094
1222
  def get_facebook_pages(self, profile: Optional[str] = None) -> Dict:
@@ -1130,6 +1258,82 @@ class UploadPostClient:
1130
1258
  params = {"profile": profile} if profile else None
1131
1259
  return self._request("/uploadposts/pinterest/boards", "GET", params=params)
1132
1260
 
1261
+ # ==================== Instagram Comments ====================
1262
+
1263
+ def get_post_comments(
1264
+ self,
1265
+ user: str,
1266
+ post_id: Optional[str] = None,
1267
+ post_url: Optional[str] = None
1268
+ ) -> Dict:
1269
+ """
1270
+ Get comments on an Instagram post.
1271
+
1272
+ Args:
1273
+ user: Profile username.
1274
+ post_id: Numeric media ID (provide post_id or post_url).
1275
+ post_url: Full Instagram post URL (provide post_id or post_url).
1276
+
1277
+ Returns:
1278
+ Comments data including comment IDs, text, timestamps, and user info.
1279
+ """
1280
+ params = {"platform": "instagram", "user": user}
1281
+ if post_id:
1282
+ params["post_id"] = post_id
1283
+ if post_url:
1284
+ params["post_url"] = post_url
1285
+ return self._request("/uploadposts/comments", "GET", params=params)
1286
+
1287
+ def reply_to_comment(
1288
+ self,
1289
+ user: str,
1290
+ comment_id: str,
1291
+ message: str
1292
+ ) -> Dict:
1293
+ """
1294
+ Send a private reply (DM) to the author of an Instagram comment.
1295
+
1296
+ Args:
1297
+ user: Profile username.
1298
+ comment_id: Comment ID from get_post_comments.
1299
+ message: Reply message text.
1300
+
1301
+ Returns:
1302
+ Reply result with recipient_id and message_id.
1303
+ """
1304
+ return self._request("/uploadposts/comments/reply", "POST", json_data={
1305
+ "platform": "instagram",
1306
+ "user": user,
1307
+ "comment_id": comment_id,
1308
+ "message": message
1309
+ })
1310
+
1311
+ def public_reply_to_comment(
1312
+ self,
1313
+ user: str,
1314
+ comment_id: str,
1315
+ message: str
1316
+ ) -> Dict:
1317
+ """
1318
+ Post a public reply to an Instagram comment (visible under the original comment).
1319
+
1320
+ Args:
1321
+ user: Profile username.
1322
+ comment_id: Comment ID from get_post_comments.
1323
+ message: Reply message text.
1324
+
1325
+ Returns:
1326
+ Reply result with the new comment ID.
1327
+ """
1328
+ return self._request("/uploadposts/comments/public-reply", "POST", json_data={
1329
+ "platform": "instagram",
1330
+ "user": user,
1331
+ "comment_id": comment_id,
1332
+ "message": message
1333
+ })
1334
+
1335
+ # ==================== Google Business ====================
1336
+
1133
1337
  def get_google_business_locations(self, profile: Optional[str] = None) -> Dict:
1134
1338
  """
1135
1339
  Get Google Business Profile locations for a connected account.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: upload-post
3
- Version: 2.1.0
3
+ Version: 2.2.0
4
4
  Summary: Cross-platform social media upload for TikTok, Instagram, YouTube, LinkedIn, Facebook, Pinterest, Threads, Reddit, Bluesky, and X (Twitter)
5
5
  Home-page: https://www.upload-post.com/
6
6
  Author: Upload-Post
@@ -223,6 +223,23 @@ analytics = client.get_analytics(
223
223
  print(analytics)
224
224
  ```
225
225
 
226
+ ### Get Media
227
+
228
+ Retrieve recent posts from a connected social account. Supported platforms:
229
+ `instagram`, `tiktok`, `youtube`, `linkedin`, `facebook`, `x`, `threads`,
230
+ `pinterest`, `bluesky`, `reddit`.
231
+
232
+ ```python
233
+ # Personal LinkedIn profile (default for non-org accounts):
234
+ media = client.get_media("linkedin", "my-profile")
235
+
236
+ # Force the personal profile of an account connected as an org admin:
237
+ media = client.get_media("linkedin", "my-profile", page_urn="me")
238
+
239
+ # Target a specific LinkedIn organization page:
240
+ media = client.get_media("linkedin", "my-profile", page_urn="12345")
241
+ ```
242
+
226
243
  ### Helper Methods
227
244
 
228
245
  ```python
File without changes