marqetive-lib 0.1.2__py3-none-any.whl → 0.1.3__py3-none-any.whl
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.
- marqetive/__init__.py +13 -13
- marqetive/core/__init__.py +1 -1
- marqetive/core/account_factory.py +2 -2
- marqetive/core/base_manager.py +4 -4
- marqetive/core/registry.py +3 -3
- marqetive/platforms/__init__.py +6 -6
- marqetive/platforms/base.py +3 -3
- marqetive/platforms/instagram/__init__.py +3 -3
- marqetive/platforms/instagram/client.py +4 -4
- marqetive/platforms/instagram/exceptions.py +1 -1
- marqetive/platforms/instagram/factory.py +5 -5
- marqetive/platforms/instagram/manager.py +4 -4
- marqetive/platforms/instagram/media.py +2 -2
- marqetive/platforms/linkedin/__init__.py +3 -3
- marqetive/platforms/linkedin/client.py +4 -4
- marqetive/platforms/linkedin/exceptions.py +1 -1
- marqetive/platforms/linkedin/factory.py +5 -5
- marqetive/platforms/linkedin/manager.py +4 -4
- marqetive/platforms/linkedin/media.py +4 -4
- marqetive/platforms/tiktok/__init__.py +3 -3
- marqetive/platforms/tiktok/client.py +319 -104
- marqetive/platforms/tiktok/exceptions.py +170 -66
- marqetive/platforms/tiktok/factory.py +5 -5
- marqetive/platforms/tiktok/manager.py +5 -5
- marqetive/platforms/tiktok/media.py +547 -159
- marqetive/platforms/twitter/__init__.py +3 -3
- marqetive/platforms/twitter/client.py +7 -53
- marqetive/platforms/twitter/exceptions.py +1 -1
- marqetive/platforms/twitter/factory.py +5 -6
- marqetive/platforms/twitter/manager.py +4 -4
- marqetive/platforms/twitter/media.py +4 -4
- marqetive/registry_init.py +10 -8
- marqetive/utils/__init__.py +3 -3
- marqetive/utils/file_handlers.py +1 -1
- marqetive/utils/oauth.py +2 -2
- marqetive/utils/token_validator.py +1 -1
- {marqetive_lib-0.1.2.dist-info → marqetive_lib-0.1.3.dist-info}/METADATA +1 -1
- marqetive_lib-0.1.3.dist-info/RECORD +47 -0
- marqetive/platforms/twitter/threads.py +0 -442
- marqetive_lib-0.1.2.dist-info/RECORD +0 -48
- {marqetive_lib-0.1.2.dist-info → marqetive_lib-0.1.3.dist-info}/WHEEL +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"""TikTok API client implementation.
|
|
2
2
|
|
|
3
3
|
This module provides a concrete implementation of the SocialMediaPlatform
|
|
4
|
-
ABC for TikTok, using the TikTok
|
|
4
|
+
ABC for TikTok, using the TikTok Content Posting API v2.
|
|
5
5
|
|
|
6
|
-
API Documentation: https://developers.tiktok.com/doc/
|
|
6
|
+
API Documentation: https://developers.tiktok.com/doc/content-posting-api-get-started
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
from datetime import datetime
|
|
@@ -11,14 +11,14 @@ from typing import Any
|
|
|
11
11
|
|
|
12
12
|
from pydantic import HttpUrl
|
|
13
13
|
|
|
14
|
-
from
|
|
15
|
-
from
|
|
14
|
+
from marqetive.platforms.base import SocialMediaPlatform
|
|
15
|
+
from marqetive.platforms.exceptions import (
|
|
16
16
|
PlatformAuthError,
|
|
17
17
|
PlatformError,
|
|
18
18
|
PostNotFoundError,
|
|
19
19
|
ValidationError,
|
|
20
20
|
)
|
|
21
|
-
from
|
|
21
|
+
from marqetive.platforms.models import (
|
|
22
22
|
AuthCredentials,
|
|
23
23
|
Comment,
|
|
24
24
|
CommentStatus,
|
|
@@ -29,14 +29,42 @@ from src.marqetive.platforms.models import (
|
|
|
29
29
|
PostStatus,
|
|
30
30
|
PostUpdateRequest,
|
|
31
31
|
)
|
|
32
|
-
from
|
|
32
|
+
from marqetive.platforms.tiktok.exceptions import TikTokErrorCode, map_tiktok_error
|
|
33
|
+
from marqetive.platforms.tiktok.media import (
|
|
34
|
+
CreatorInfo,
|
|
35
|
+
MediaUploadResult,
|
|
36
|
+
PrivacyLevel,
|
|
37
|
+
TikTokMediaManager,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# TikTok API base URL
|
|
41
|
+
TIKTOK_API_BASE = "https://open.tiktokapis.com/v2"
|
|
42
|
+
|
|
43
|
+
# Video query fields
|
|
44
|
+
VIDEO_QUERY_FIELDS = [
|
|
45
|
+
"id",
|
|
46
|
+
"title",
|
|
47
|
+
"video_description",
|
|
48
|
+
"create_time",
|
|
49
|
+
"cover_image_url",
|
|
50
|
+
"share_url",
|
|
51
|
+
"duration",
|
|
52
|
+
"height",
|
|
53
|
+
"width",
|
|
54
|
+
"like_count",
|
|
55
|
+
"comment_count",
|
|
56
|
+
"share_count",
|
|
57
|
+
"view_count",
|
|
58
|
+
]
|
|
33
59
|
|
|
34
60
|
|
|
35
61
|
class TikTokClient(SocialMediaPlatform):
|
|
36
62
|
"""TikTok API client.
|
|
37
63
|
|
|
38
64
|
This client implements the SocialMediaPlatform interface for TikTok,
|
|
39
|
-
focusing on video uploads and management.
|
|
65
|
+
focusing on video uploads and management using the Content Posting API v2.
|
|
66
|
+
|
|
67
|
+
Note: Some operations (delete, update, comments) are not supported by TikTok API.
|
|
40
68
|
"""
|
|
41
69
|
|
|
42
70
|
def __init__(
|
|
@@ -44,20 +72,33 @@ class TikTokClient(SocialMediaPlatform):
|
|
|
44
72
|
credentials: AuthCredentials,
|
|
45
73
|
timeout: float = 300.0,
|
|
46
74
|
) -> None:
|
|
47
|
-
"""Initialize TikTok client.
|
|
48
|
-
|
|
75
|
+
"""Initialize TikTok client.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
credentials: OAuth credentials with access_token and open_id in additional_data.
|
|
79
|
+
timeout: Request timeout in seconds (default 300s for video processing).
|
|
80
|
+
"""
|
|
49
81
|
super().__init__(
|
|
50
82
|
platform_name="tiktok",
|
|
51
83
|
credentials=credentials,
|
|
52
|
-
base_url=
|
|
84
|
+
base_url=TIKTOK_API_BASE,
|
|
53
85
|
timeout=timeout,
|
|
54
86
|
)
|
|
55
87
|
self._media_manager: TikTokMediaManager | None = None
|
|
88
|
+
self._creator_info: CreatorInfo | None = None
|
|
56
89
|
|
|
57
90
|
async def _setup_managers(self) -> None:
|
|
58
91
|
"""Setup media manager."""
|
|
59
|
-
if not self.credentials.access_token
|
|
60
|
-
raise PlatformAuthError("Access token
|
|
92
|
+
if not self.credentials.access_token:
|
|
93
|
+
raise PlatformAuthError("Access token is required", "tiktok")
|
|
94
|
+
|
|
95
|
+
if (
|
|
96
|
+
not self.credentials.additional_data
|
|
97
|
+
or "open_id" not in self.credentials.additional_data
|
|
98
|
+
):
|
|
99
|
+
raise PlatformAuthError(
|
|
100
|
+
"open_id is required in additional_data", "tiktok"
|
|
101
|
+
)
|
|
61
102
|
|
|
62
103
|
self._media_manager = TikTokMediaManager(
|
|
63
104
|
access_token=self.credentials.access_token,
|
|
@@ -90,29 +131,62 @@ class TikTokClient(SocialMediaPlatform):
|
|
|
90
131
|
)
|
|
91
132
|
|
|
92
133
|
async def refresh_token(self) -> AuthCredentials:
|
|
93
|
-
"""Refresh TikTok access token.
|
|
94
|
-
|
|
95
|
-
|
|
134
|
+
"""Refresh TikTok access token.
|
|
135
|
+
|
|
136
|
+
Note: Token refresh should be handled by the AccountFactory.
|
|
137
|
+
This method just returns the current credentials.
|
|
138
|
+
"""
|
|
96
139
|
return self.credentials
|
|
97
140
|
|
|
98
141
|
async def is_authenticated(self) -> bool:
|
|
99
|
-
"""Check if TikTok credentials are valid.
|
|
100
|
-
|
|
142
|
+
"""Check if TikTok credentials are valid.
|
|
143
|
+
|
|
144
|
+
Validates credentials by fetching user info from the API.
|
|
145
|
+
"""
|
|
146
|
+
if not self.api_client:
|
|
101
147
|
return False
|
|
102
148
|
try:
|
|
103
149
|
# Verify credentials by fetching authenticated user info
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
"
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
150
|
+
# TikTok requires 'fields' as query parameter
|
|
151
|
+
response = await self.api_client.get(
|
|
152
|
+
"user/info/",
|
|
153
|
+
params={"fields": "open_id,union_id,avatar_url,display_name"},
|
|
154
|
+
)
|
|
155
|
+
data = response.data
|
|
156
|
+
|
|
157
|
+
# Check for error in response
|
|
158
|
+
error_code = data.get("error", {}).get("code", "")
|
|
159
|
+
if error_code and error_code != TikTokErrorCode.OK:
|
|
160
|
+
return False
|
|
161
|
+
|
|
162
|
+
return data.get("data", {}).get("user") is not None
|
|
110
163
|
except PlatformError:
|
|
111
164
|
return False
|
|
112
165
|
|
|
166
|
+
async def query_creator_info(self) -> CreatorInfo:
|
|
167
|
+
"""Query creator info before posting.
|
|
168
|
+
|
|
169
|
+
This must be called before creating a post to get available
|
|
170
|
+
privacy levels and posting limits.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
CreatorInfo with available options for this creator.
|
|
174
|
+
"""
|
|
175
|
+
if not self._media_manager:
|
|
176
|
+
raise RuntimeError("Client must be used as async context manager")
|
|
177
|
+
|
|
178
|
+
self._creator_info = await self._media_manager.query_creator_info()
|
|
179
|
+
return self._creator_info
|
|
180
|
+
|
|
113
181
|
async def create_post(self, request: PostCreateRequest) -> Post:
|
|
114
182
|
"""Create and publish a TikTok video.
|
|
115
183
|
|
|
184
|
+
TikTok requires a video to be uploaded. The upload flow is:
|
|
185
|
+
1. Query creator info (required)
|
|
186
|
+
2. Initialize upload (get publish_id and upload_url)
|
|
187
|
+
3. Upload video chunks
|
|
188
|
+
4. Poll status until PUBLISH_COMPLETE
|
|
189
|
+
|
|
116
190
|
Args:
|
|
117
191
|
request: The post creation request, must contain a video URL.
|
|
118
192
|
|
|
@@ -132,128 +206,265 @@ class TikTokClient(SocialMediaPlatform):
|
|
|
132
206
|
platform=self.platform_name,
|
|
133
207
|
)
|
|
134
208
|
|
|
135
|
-
# 1.
|
|
209
|
+
# 1. Query creator info (required before posting)
|
|
210
|
+
if not self._creator_info:
|
|
211
|
+
await self.query_creator_info()
|
|
212
|
+
|
|
213
|
+
# 2. Determine privacy level
|
|
214
|
+
privacy_level = PrivacyLevel.SELF_ONLY # Default for unaudited apps
|
|
215
|
+
requested_privacy = request.additional_data.get("privacy_level")
|
|
216
|
+
if requested_privacy and self._creator_info:
|
|
217
|
+
# Check if requested privacy is available
|
|
218
|
+
available_privacies = self._creator_info.privacy_level_options or []
|
|
219
|
+
if requested_privacy in available_privacies:
|
|
220
|
+
privacy_level = PrivacyLevel(requested_privacy)
|
|
221
|
+
|
|
222
|
+
# 3. Upload video and wait for publish
|
|
136
223
|
video_url = request.media_urls[0]
|
|
137
|
-
upload_result = await self._media_manager.upload_media(
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
"
|
|
147
|
-
"
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
"video_id": upload_result.media_id,
|
|
154
|
-
},
|
|
155
|
-
}
|
|
156
|
-
response = await self.api_client.post("video/publish/", data=payload)
|
|
157
|
-
post_id = response.data.get("data", {}).get("publish_id")
|
|
158
|
-
|
|
159
|
-
if not post_id:
|
|
160
|
-
raise PlatformError("Post creation succeeded but no publish_id returned.")
|
|
161
|
-
|
|
162
|
-
# 3. Fetch the created post to return a full Post object
|
|
163
|
-
return await self.get_post(post_id)
|
|
224
|
+
upload_result = await self._media_manager.upload_media(
|
|
225
|
+
video_url,
|
|
226
|
+
title=request.content or "",
|
|
227
|
+
privacy_level=privacy_level,
|
|
228
|
+
wait_for_publish=True,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
if not upload_result.video_id:
|
|
232
|
+
raise PlatformError(
|
|
233
|
+
"Video upload succeeded but no video ID returned. "
|
|
234
|
+
"Video may still be processing.",
|
|
235
|
+
platform=self.platform_name,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
# 4. Fetch the created post to return full Post object
|
|
239
|
+
return await self.get_post(upload_result.video_id)
|
|
164
240
|
|
|
165
241
|
async def get_post(self, post_id: str) -> Post:
|
|
166
|
-
"""Retrieve a TikTok video by its ID.
|
|
167
|
-
|
|
242
|
+
"""Retrieve a TikTok video by its ID.
|
|
243
|
+
|
|
244
|
+
Uses POST /video/query/ endpoint with filters.video_ids.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
post_id: The video ID to retrieve.
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
Post object with video details.
|
|
251
|
+
|
|
252
|
+
Raises:
|
|
253
|
+
PostNotFoundError: If the video doesn't exist.
|
|
254
|
+
"""
|
|
255
|
+
if not self.api_client:
|
|
168
256
|
raise RuntimeError("Client must be used as async context manager")
|
|
257
|
+
|
|
169
258
|
try:
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
"
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
259
|
+
# TikTok uses POST for video query with body
|
|
260
|
+
response = await self.api_client.post(
|
|
261
|
+
"video/query/",
|
|
262
|
+
data={
|
|
263
|
+
"filters": {"video_ids": [post_id]},
|
|
264
|
+
"fields": VIDEO_QUERY_FIELDS,
|
|
265
|
+
},
|
|
266
|
+
)
|
|
267
|
+
data = response.data
|
|
268
|
+
|
|
269
|
+
# Check for API errors
|
|
270
|
+
error_code = data.get("error", {}).get("code", "")
|
|
271
|
+
if error_code and error_code != TikTokErrorCode.OK:
|
|
272
|
+
raise map_tiktok_error(
|
|
273
|
+
status_code=response.status_code,
|
|
274
|
+
error_code=error_code,
|
|
275
|
+
error_message=data.get("error", {}).get("message"),
|
|
276
|
+
response_data=data,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
videos = data.get("data", {}).get("videos", [])
|
|
280
|
+
if not videos:
|
|
177
281
|
raise PostNotFoundError(post_id, self.platform_name)
|
|
178
|
-
|
|
282
|
+
|
|
283
|
+
return self._parse_video_post(videos[0])
|
|
284
|
+
|
|
285
|
+
except PostNotFoundError:
|
|
286
|
+
raise
|
|
179
287
|
except PlatformError as e:
|
|
180
288
|
raise PostNotFoundError(
|
|
181
289
|
post_id, self.platform_name, status_code=e.status_code
|
|
182
290
|
) from e
|
|
183
291
|
|
|
184
|
-
async def update_post(
|
|
185
|
-
|
|
292
|
+
async def update_post(
|
|
293
|
+
self, post_id: str, request: PostUpdateRequest # noqa: ARG002
|
|
294
|
+
) -> Post:
|
|
295
|
+
"""Update a TikTok video.
|
|
296
|
+
|
|
297
|
+
TikTok API does not support updating videos after publishing.
|
|
298
|
+
"""
|
|
186
299
|
raise PlatformError(
|
|
187
|
-
"TikTok API does not support updating videos.",
|
|
300
|
+
"TikTok API does not support updating videos after publishing.",
|
|
301
|
+
self.platform_name,
|
|
188
302
|
)
|
|
189
303
|
|
|
190
|
-
async def delete_post(self, post_id: str) -> bool:
|
|
191
|
-
"""Delete a TikTok video.
|
|
304
|
+
async def delete_post(self, post_id: str) -> bool: # noqa: ARG002
|
|
305
|
+
"""Delete a TikTok video.
|
|
306
|
+
|
|
307
|
+
TikTok API does not support deleting videos via API.
|
|
308
|
+
Users must delete videos through the TikTok app.
|
|
309
|
+
"""
|
|
192
310
|
raise PlatformError(
|
|
193
|
-
"TikTok API does not support deleting videos."
|
|
311
|
+
"TikTok API does not support deleting videos. "
|
|
312
|
+
"Please delete the video through the TikTok app.",
|
|
313
|
+
self.platform_name,
|
|
194
314
|
)
|
|
195
315
|
|
|
196
316
|
async def get_comments(
|
|
197
|
-
self,
|
|
317
|
+
self,
|
|
318
|
+
post_id: str, # noqa: ARG002
|
|
319
|
+
limit: int = 20, # noqa: ARG002
|
|
320
|
+
offset: int = 0, # noqa: ARG002
|
|
198
321
|
) -> list[Comment]:
|
|
199
|
-
"""Retrieve comments for a TikTok video.
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}
|
|
209
|
-
response = await self.api_client.get("video/comment/list/", params=params)
|
|
210
|
-
comments_data = response.data.get("data", {}).get("comments", [])
|
|
211
|
-
return [self._parse_comment(c, post_id) for c in comments_data]
|
|
212
|
-
|
|
213
|
-
async def create_comment(self, post_id: str, content: str) -> Comment:
|
|
214
|
-
"""Create a comment on a TikTok video."""
|
|
215
|
-
raise PlatformError("Commenting via API is not supported.", self.platform_name)
|
|
216
|
-
|
|
217
|
-
async def delete_comment(self, comment_id: str) -> bool:
|
|
218
|
-
"""Delete a comment on a TikTok video."""
|
|
322
|
+
"""Retrieve comments for a TikTok video.
|
|
323
|
+
|
|
324
|
+
Note: The standard TikTok API v2 does NOT provide access to video comments.
|
|
325
|
+
Only comment_count is available in video metadata.
|
|
326
|
+
The Comments API is only available via the Research API for academic use.
|
|
327
|
+
|
|
328
|
+
Raises:
|
|
329
|
+
PlatformError: Always, as this feature is not available.
|
|
330
|
+
"""
|
|
219
331
|
raise PlatformError(
|
|
220
|
-
"
|
|
332
|
+
"TikTok's standard API does not provide access to video comments. "
|
|
333
|
+
"Comments are only available via the Research API for academic researchers.",
|
|
334
|
+
self.platform_name,
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
async def create_comment(
|
|
338
|
+
self, post_id: str, content: str # noqa: ARG002
|
|
339
|
+
) -> Comment:
|
|
340
|
+
"""Create a comment on a TikTok video.
|
|
341
|
+
|
|
342
|
+
TikTok API does not support creating comments programmatically.
|
|
343
|
+
"""
|
|
344
|
+
raise PlatformError(
|
|
345
|
+
"TikTok API does not support creating comments.",
|
|
346
|
+
self.platform_name,
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
async def delete_comment(self, comment_id: str) -> bool: # noqa: ARG002
|
|
350
|
+
"""Delete a comment on a TikTok video.
|
|
351
|
+
|
|
352
|
+
TikTok API does not support deleting comments programmatically.
|
|
353
|
+
"""
|
|
354
|
+
raise PlatformError(
|
|
355
|
+
"TikTok API does not support deleting comments.",
|
|
356
|
+
self.platform_name,
|
|
221
357
|
)
|
|
222
358
|
|
|
223
359
|
async def upload_media(
|
|
224
|
-
self, media_url: str, media_type: str, alt_text: str | None = None
|
|
360
|
+
self, media_url: str, media_type: str, alt_text: str | None = None # noqa: ARG002
|
|
225
361
|
) -> MediaAttachment:
|
|
226
|
-
"""Upload a video to TikTok.
|
|
362
|
+
"""Upload a video to TikTok.
|
|
363
|
+
|
|
364
|
+
This initiates the upload flow but doesn't publish.
|
|
365
|
+
Use create_post() for full upload + publish flow.
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
media_url: URL or path to the video file.
|
|
369
|
+
media_type: Must be "video" for TikTok.
|
|
370
|
+
alt_text: Not used (TikTok doesn't support alt text).
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
MediaAttachment with publish_id as media_id.
|
|
374
|
+
|
|
375
|
+
Raises:
|
|
376
|
+
ValidationError: If media_type is not "video".
|
|
377
|
+
"""
|
|
227
378
|
if not self._media_manager:
|
|
228
379
|
raise RuntimeError("Client not initialized. Use as async context manager.")
|
|
380
|
+
|
|
229
381
|
if media_type != "video":
|
|
230
|
-
raise ValidationError(
|
|
382
|
+
raise ValidationError(
|
|
383
|
+
"Only video media type is supported for TikTok.",
|
|
384
|
+
platform=self.platform_name,
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
# Upload without waiting for publish
|
|
388
|
+
result = await self._media_manager.upload_media(
|
|
389
|
+
media_url,
|
|
390
|
+
wait_for_publish=False,
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
# Use original URL if it's a valid HTTP URL, otherwise use a placeholder
|
|
394
|
+
url = (
|
|
395
|
+
HttpUrl(media_url)
|
|
396
|
+
if media_url.startswith("http")
|
|
397
|
+
else HttpUrl("https://www.tiktok.com/")
|
|
398
|
+
)
|
|
231
399
|
|
|
232
|
-
result = await self._media_manager.upload_media(media_url)
|
|
233
400
|
return MediaAttachment(
|
|
234
|
-
media_id=result.
|
|
401
|
+
media_id=result.publish_id,
|
|
235
402
|
media_type=MediaType.VIDEO,
|
|
236
|
-
url=
|
|
403
|
+
url=url,
|
|
237
404
|
)
|
|
238
405
|
|
|
406
|
+
async def check_publish_status(self, publish_id: str) -> MediaUploadResult:
|
|
407
|
+
"""Check the publish status of an upload.
|
|
408
|
+
|
|
409
|
+
Args:
|
|
410
|
+
publish_id: The publish_id from upload_media.
|
|
411
|
+
|
|
412
|
+
Returns:
|
|
413
|
+
MediaUploadResult with current status and video_id if complete.
|
|
414
|
+
"""
|
|
415
|
+
if not self._media_manager:
|
|
416
|
+
raise RuntimeError("Client not initialized. Use as async context manager.")
|
|
417
|
+
|
|
418
|
+
return await self._media_manager.check_publish_status(publish_id)
|
|
419
|
+
|
|
420
|
+
async def wait_for_publish(self, publish_id: str) -> MediaUploadResult:
|
|
421
|
+
"""Wait for a video to finish publishing.
|
|
422
|
+
|
|
423
|
+
Args:
|
|
424
|
+
publish_id: The publish_id from upload_media.
|
|
425
|
+
|
|
426
|
+
Returns:
|
|
427
|
+
MediaUploadResult with video_id once published.
|
|
428
|
+
"""
|
|
429
|
+
if not self._media_manager:
|
|
430
|
+
raise RuntimeError("Client not initialized. Use as async context manager.")
|
|
431
|
+
|
|
432
|
+
return await self._media_manager.wait_for_publish(publish_id)
|
|
433
|
+
|
|
239
434
|
def _parse_video_post(self, video_data: dict[str, Any]) -> Post:
|
|
240
|
-
"""Parse a TikTok API video object into a Post model.
|
|
435
|
+
"""Parse a TikTok API video object into a Post model.
|
|
436
|
+
|
|
437
|
+
Args:
|
|
438
|
+
video_data: Raw video data from TikTok API.
|
|
439
|
+
|
|
440
|
+
Returns:
|
|
441
|
+
Post model with video details.
|
|
442
|
+
"""
|
|
443
|
+
# TikTok uses 'id' field for video ID
|
|
444
|
+
video_id = video_data.get("id", video_data.get("video_id", ""))
|
|
445
|
+
|
|
446
|
+
# Handle share_url - use placeholder if empty
|
|
447
|
+
share_url = video_data.get("share_url", "")
|
|
448
|
+
media_url = (
|
|
449
|
+
HttpUrl(share_url) if share_url else HttpUrl("https://www.tiktok.com/")
|
|
450
|
+
)
|
|
451
|
+
|
|
241
452
|
return Post(
|
|
242
|
-
post_id=
|
|
453
|
+
post_id=video_id,
|
|
243
454
|
platform=self.platform_name,
|
|
244
|
-
content=video_data.get("title", ""),
|
|
455
|
+
content=video_data.get("title", video_data.get("video_description", "")),
|
|
245
456
|
media=[
|
|
246
457
|
MediaAttachment(
|
|
247
|
-
media_id=
|
|
458
|
+
media_id=video_id,
|
|
248
459
|
media_type=MediaType.VIDEO,
|
|
249
|
-
url=
|
|
460
|
+
url=media_url,
|
|
250
461
|
width=video_data.get("width"),
|
|
251
462
|
height=video_data.get("height"),
|
|
252
463
|
)
|
|
253
464
|
],
|
|
254
465
|
status=PostStatus.PUBLISHED,
|
|
255
466
|
created_at=datetime.fromtimestamp(video_data.get("create_time", 0)),
|
|
256
|
-
author_id=
|
|
467
|
+
author_id=video_data.get("open_id", ""),
|
|
257
468
|
likes_count=video_data.get("like_count", 0),
|
|
258
469
|
comments_count=video_data.get("comment_count", 0),
|
|
259
470
|
shares_count=video_data.get("share_count", 0),
|
|
@@ -262,16 +473,20 @@ class TikTokClient(SocialMediaPlatform):
|
|
|
262
473
|
)
|
|
263
474
|
|
|
264
475
|
def _parse_comment(self, comment_data: dict[str, Any], post_id: str) -> Comment:
|
|
265
|
-
"""Parse a TikTok API comment object into a Comment model.
|
|
476
|
+
"""Parse a TikTok API comment object into a Comment model.
|
|
477
|
+
|
|
478
|
+
Note: This method exists for interface completeness but comments
|
|
479
|
+
are not accessible via standard TikTok API.
|
|
480
|
+
"""
|
|
266
481
|
return Comment(
|
|
267
|
-
comment_id=comment_data
|
|
482
|
+
comment_id=comment_data.get("id", ""),
|
|
268
483
|
post_id=post_id,
|
|
269
484
|
platform=self.platform_name,
|
|
270
485
|
content=comment_data.get("text", ""),
|
|
271
486
|
author_id=comment_data.get("open_id", ""),
|
|
272
487
|
created_at=datetime.fromtimestamp(comment_data.get("create_time", 0)),
|
|
273
488
|
likes_count=comment_data.get("like_count", 0),
|
|
274
|
-
replies_count=0,
|
|
489
|
+
replies_count=comment_data.get("reply_count", 0),
|
|
275
490
|
status=CommentStatus.VISIBLE,
|
|
276
491
|
raw_data=comment_data,
|
|
277
492
|
)
|