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