@tikomni/skills 0.1.7 → 0.1.9
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.
- package/package.json +1 -1
- package/skills/social-media-crawl/scripts/core/tikomni_common.py +7 -0
- package/skills/social-media-crawl/scripts/core/u3_fallback.py +146 -28
- package/skills/social-media-crawl/scripts/pipelines/douyin_metadata.py +151 -0
- package/skills/social-media-crawl/scripts/pipelines/home_asr.py +40 -37
- package/skills/social-media-crawl/scripts/pipelines/homepage_collectors.py +5 -11
- package/skills/social-media-crawl/scripts/pipelines/input_contracts.py +318 -0
- package/skills/social-media-crawl/scripts/pipelines/media_url_rules.py +86 -0
- package/skills/social-media-crawl/scripts/pipelines/platform_adapters.py +77 -30
- package/skills/social-media-crawl/scripts/pipelines/run_douyin_creator_home.py +84 -6
- package/skills/social-media-crawl/scripts/pipelines/run_douyin_single_work.py +79 -73
- package/skills/social-media-crawl/scripts/pipelines/run_xiaohongshu_creator_home.py +84 -6
- package/skills/social-media-crawl/scripts/pipelines/run_xiaohongshu_single_work.py +86 -60
- package/skills/social-media-crawl/scripts/writers/write_work_fact_card.py +5 -3
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Shared input normalization and validation for social-media pipelines."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
from typing import Dict, List, Optional
|
|
8
|
+
from urllib.parse import parse_qs, urlparse
|
|
9
|
+
|
|
10
|
+
from scripts.core.tikomni_common import normalize_text
|
|
11
|
+
|
|
12
|
+
_HTTP_URL_RE = re.compile(r"https?://[^\s<>'\",。!?;:)】》]+", re.IGNORECASE)
|
|
13
|
+
_URL_TRAILING_PUNCTUATION = ".,!?;:)]}>'\",。!?;:)】》、"
|
|
14
|
+
_XHS_NOTE_ID_RE = re.compile(r"^[0-9A-Za-z]{16,32}$")
|
|
15
|
+
_XHS_USER_ID_RE = re.compile(r"^[0-9A-Za-z]{8,32}$")
|
|
16
|
+
_DOUYIN_SEC_UID_RE = re.compile(r"^MS4wLjA[A-Za-z0-9_-]{8,}$")
|
|
17
|
+
|
|
18
|
+
_DOUYIN_HOST_TOKENS = ("douyin.com", "iesdouyin.com", "v.douyin.com")
|
|
19
|
+
_XHS_HOST_TOKENS = ("xiaohongshu.com", "xhslink.com")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _dedupe_keep_order(items: List[str]) -> List[str]:
|
|
23
|
+
unique: List[str] = []
|
|
24
|
+
seen = set()
|
|
25
|
+
for item in items:
|
|
26
|
+
if item in seen:
|
|
27
|
+
continue
|
|
28
|
+
unique.append(item)
|
|
29
|
+
seen.add(item)
|
|
30
|
+
return unique
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _strip_url_punctuation(value: str) -> str:
|
|
34
|
+
return value.rstrip(_URL_TRAILING_PUNCTUATION)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def extract_http_urls(text: Optional[str]) -> List[str]:
|
|
38
|
+
raw = normalize_text(text)
|
|
39
|
+
if not raw:
|
|
40
|
+
return []
|
|
41
|
+
matches = [_strip_url_punctuation(match.group(0)) for match in _HTTP_URL_RE.finditer(raw)]
|
|
42
|
+
return _dedupe_keep_order([item for item in matches if item])
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _first_platform_url(text: Optional[str], host_tokens: tuple[str, ...]) -> Optional[str]:
|
|
46
|
+
for url in extract_http_urls(text):
|
|
47
|
+
host = urlparse(url).netloc.lower()
|
|
48
|
+
if any(token in host for token in host_tokens):
|
|
49
|
+
return url
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _text_contains_host(text: Optional[str], host_tokens: tuple[str, ...]) -> bool:
|
|
54
|
+
lowered = normalize_text(text).lower()
|
|
55
|
+
return any(token in lowered for token in host_tokens)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def looks_like_douyin_sec_uid(value: Optional[str]) -> bool:
|
|
59
|
+
return bool(_DOUYIN_SEC_UID_RE.fullmatch(normalize_text(value)))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def looks_like_xhs_note_id(value: Optional[str]) -> bool:
|
|
63
|
+
return bool(_XHS_NOTE_ID_RE.fullmatch(normalize_text(value)))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def looks_like_xhs_user_id(value: Optional[str]) -> bool:
|
|
67
|
+
return bool(_XHS_USER_ID_RE.fullmatch(normalize_text(value)))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def extract_douyin_share_url(text: Optional[str]) -> Optional[str]:
|
|
71
|
+
return _first_platform_url(text, _DOUYIN_HOST_TOKENS)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def extract_douyin_sec_uid(text: Optional[str]) -> Optional[str]:
|
|
75
|
+
raw = normalize_text(text)
|
|
76
|
+
if not raw:
|
|
77
|
+
return None
|
|
78
|
+
if looks_like_douyin_sec_uid(raw):
|
|
79
|
+
return raw
|
|
80
|
+
|
|
81
|
+
for url in extract_http_urls(raw):
|
|
82
|
+
query = parse_qs(urlparse(url).query)
|
|
83
|
+
for key in ("sec_uid", "sec_user_id"):
|
|
84
|
+
candidate = normalize_text((query.get(key) or [""])[0])
|
|
85
|
+
if looks_like_douyin_sec_uid(candidate):
|
|
86
|
+
return candidate
|
|
87
|
+
|
|
88
|
+
match = re.search(r"(?:sec_uid|sec_user_id)=([A-Za-z0-9._-]+)", raw)
|
|
89
|
+
if match:
|
|
90
|
+
candidate = normalize_text(match.group(1))
|
|
91
|
+
if looks_like_douyin_sec_uid(candidate):
|
|
92
|
+
return candidate
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def extract_xhs_note_id(text: Optional[str]) -> Optional[str]:
|
|
97
|
+
raw = normalize_text(text)
|
|
98
|
+
if not raw:
|
|
99
|
+
return None
|
|
100
|
+
if looks_like_xhs_note_id(raw):
|
|
101
|
+
return raw
|
|
102
|
+
|
|
103
|
+
for pattern in (
|
|
104
|
+
r"/explore/([0-9A-Za-z]+)",
|
|
105
|
+
r"/discovery/item/([0-9A-Za-z]+)",
|
|
106
|
+
r"note_id=([0-9A-Za-z]+)",
|
|
107
|
+
):
|
|
108
|
+
match = re.search(pattern, raw)
|
|
109
|
+
if not match:
|
|
110
|
+
continue
|
|
111
|
+
candidate = normalize_text(match.group(1))
|
|
112
|
+
if looks_like_xhs_note_id(candidate):
|
|
113
|
+
return candidate
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def extract_xhs_user_id(text: Optional[str]) -> Optional[str]:
|
|
118
|
+
raw = normalize_text(text)
|
|
119
|
+
if not raw:
|
|
120
|
+
return None
|
|
121
|
+
if looks_like_xhs_user_id(raw):
|
|
122
|
+
return raw
|
|
123
|
+
|
|
124
|
+
for pattern in (
|
|
125
|
+
r"/user/profile/([0-9A-Za-z]+)",
|
|
126
|
+
r"(?:user_id|userid)=([0-9A-Za-z]+)",
|
|
127
|
+
):
|
|
128
|
+
match = re.search(pattern, raw)
|
|
129
|
+
if not match:
|
|
130
|
+
continue
|
|
131
|
+
candidate = normalize_text(match.group(1))
|
|
132
|
+
if looks_like_xhs_user_id(candidate):
|
|
133
|
+
return candidate
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def extract_xhs_note_share_url(text: Optional[str]) -> Optional[str]:
|
|
138
|
+
for url in extract_http_urls(text):
|
|
139
|
+
parsed = urlparse(url)
|
|
140
|
+
host = parsed.netloc.lower()
|
|
141
|
+
path = parsed.path.lower()
|
|
142
|
+
query = parsed.query.lower()
|
|
143
|
+
if "xhslink.com" in host:
|
|
144
|
+
return url
|
|
145
|
+
if "xiaohongshu.com" not in host:
|
|
146
|
+
continue
|
|
147
|
+
if "/explore/" in path or "/discovery/item/" in path or "note_id=" in query:
|
|
148
|
+
return url
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def extract_xhs_creator_share_url(text: Optional[str]) -> Optional[str]:
|
|
153
|
+
for url in extract_http_urls(text):
|
|
154
|
+
parsed = urlparse(url)
|
|
155
|
+
host = parsed.netloc.lower()
|
|
156
|
+
path = parsed.path.lower()
|
|
157
|
+
query = parsed.query.lower()
|
|
158
|
+
if "xhslink.com" in host:
|
|
159
|
+
return url
|
|
160
|
+
if "xiaohongshu.com" not in host:
|
|
161
|
+
continue
|
|
162
|
+
if "/user/profile/" in path or "/user/" in path or "user_id=" in query or "userid=" in query:
|
|
163
|
+
return url
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def text_has_douyin_short_link(text: Optional[str]) -> bool:
|
|
168
|
+
return _text_contains_host(text, ("v.douyin.com",))
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def text_has_xhs_short_link(text: Optional[str]) -> bool:
|
|
172
|
+
return _text_contains_host(text, ("xhslink.com",))
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def normalize_douyin_work_input(input_value: Optional[str], share_url: Optional[str]) -> Dict[str, object]:
|
|
176
|
+
explicit_share = normalize_text(share_url)
|
|
177
|
+
raw_input = normalize_text(input_value)
|
|
178
|
+
normalized_share = extract_douyin_share_url(explicit_share) or extract_douyin_share_url(raw_input)
|
|
179
|
+
|
|
180
|
+
if normalized_share:
|
|
181
|
+
return {
|
|
182
|
+
"share_url": normalized_share,
|
|
183
|
+
"error_reason": None,
|
|
184
|
+
"missing_fields": [],
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if raw_input:
|
|
188
|
+
if text_has_douyin_short_link(raw_input):
|
|
189
|
+
return {
|
|
190
|
+
"share_url": None,
|
|
191
|
+
"error_reason": "short_link_detected_but_unresolved",
|
|
192
|
+
"missing_fields": [{"field": "share_url", "reason": "short_link_unresolved"}],
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
"share_url": None,
|
|
196
|
+
"error_reason": "invalid_share_url",
|
|
197
|
+
"missing_fields": [{"field": "share_url", "reason": "invalid_input"}],
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
"share_url": None,
|
|
202
|
+
"error_reason": None,
|
|
203
|
+
"missing_fields": [],
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def normalize_xhs_note_input(
|
|
208
|
+
input_value: Optional[str],
|
|
209
|
+
share_text: Optional[str],
|
|
210
|
+
note_id: Optional[str],
|
|
211
|
+
) -> Dict[str, object]:
|
|
212
|
+
explicit_note_id = normalize_text(note_id)
|
|
213
|
+
if explicit_note_id and not looks_like_xhs_note_id(explicit_note_id):
|
|
214
|
+
return {
|
|
215
|
+
"share_text": None,
|
|
216
|
+
"note_id": None,
|
|
217
|
+
"error_reason": "invalid_note_id",
|
|
218
|
+
"missing_fields": [{"field": "note_id", "reason": "invalid_format"}],
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
explicit_share = normalize_text(share_text)
|
|
222
|
+
raw_input = normalize_text(input_value)
|
|
223
|
+
|
|
224
|
+
normalized_note_id = explicit_note_id or extract_xhs_note_id(explicit_share) or extract_xhs_note_id(raw_input)
|
|
225
|
+
normalized_share = extract_xhs_note_share_url(explicit_share) or extract_xhs_note_share_url(raw_input)
|
|
226
|
+
|
|
227
|
+
if normalized_note_id or normalized_share:
|
|
228
|
+
return {
|
|
229
|
+
"share_text": normalized_share or None,
|
|
230
|
+
"note_id": normalized_note_id or None,
|
|
231
|
+
"error_reason": None,
|
|
232
|
+
"missing_fields": [],
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
candidate = explicit_share or raw_input
|
|
236
|
+
if candidate:
|
|
237
|
+
if text_has_xhs_short_link(candidate):
|
|
238
|
+
return {
|
|
239
|
+
"share_text": None,
|
|
240
|
+
"note_id": None,
|
|
241
|
+
"error_reason": "short_link_detected_but_unresolved",
|
|
242
|
+
"missing_fields": [{"field": "note_id", "reason": "short_link_unresolved"}],
|
|
243
|
+
}
|
|
244
|
+
return {
|
|
245
|
+
"share_text": None,
|
|
246
|
+
"note_id": None,
|
|
247
|
+
"error_reason": "invalid_note_id",
|
|
248
|
+
"missing_fields": [{"field": "note_id", "reason": "invalid_format"}],
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
"share_text": None,
|
|
253
|
+
"note_id": None,
|
|
254
|
+
"error_reason": None,
|
|
255
|
+
"missing_fields": [],
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def normalize_douyin_creator_input(input_value: Optional[str]) -> Dict[str, object]:
|
|
260
|
+
raw_input = normalize_text(input_value)
|
|
261
|
+
normalized_input = extract_douyin_sec_uid(raw_input) or extract_douyin_share_url(raw_input) or raw_input or None
|
|
262
|
+
|
|
263
|
+
if extract_douyin_sec_uid(raw_input) or extract_douyin_share_url(raw_input):
|
|
264
|
+
return {
|
|
265
|
+
"input_value": normalized_input,
|
|
266
|
+
"error_reason": None,
|
|
267
|
+
"missing_fields": [],
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if raw_input:
|
|
271
|
+
if text_has_douyin_short_link(raw_input):
|
|
272
|
+
return {
|
|
273
|
+
"input_value": None,
|
|
274
|
+
"error_reason": "short_link_detected_but_unresolved",
|
|
275
|
+
"missing_fields": [{"field": "platform_author_id", "reason": "short_link_unresolved"}],
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
"input_value": None,
|
|
279
|
+
"error_reason": "invalid_creator_input",
|
|
280
|
+
"missing_fields": [{"field": "platform_author_id", "reason": "invalid_format"}],
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
"input_value": None,
|
|
285
|
+
"error_reason": None,
|
|
286
|
+
"missing_fields": [],
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def normalize_xhs_creator_input(input_value: Optional[str]) -> Dict[str, object]:
|
|
291
|
+
raw_input = normalize_text(input_value)
|
|
292
|
+
normalized_input = extract_xhs_user_id(raw_input) or extract_xhs_creator_share_url(raw_input) or None
|
|
293
|
+
|
|
294
|
+
if normalized_input:
|
|
295
|
+
return {
|
|
296
|
+
"input_value": normalized_input,
|
|
297
|
+
"error_reason": None,
|
|
298
|
+
"missing_fields": [],
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if raw_input:
|
|
302
|
+
if text_has_xhs_short_link(raw_input):
|
|
303
|
+
return {
|
|
304
|
+
"input_value": None,
|
|
305
|
+
"error_reason": "short_link_detected_but_unresolved",
|
|
306
|
+
"missing_fields": [{"field": "platform_author_id", "reason": "short_link_unresolved"}],
|
|
307
|
+
}
|
|
308
|
+
return {
|
|
309
|
+
"input_value": None,
|
|
310
|
+
"error_reason": "invalid_creator_input",
|
|
311
|
+
"missing_fields": [{"field": "platform_author_id", "reason": "invalid_format"}],
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
"input_value": None,
|
|
316
|
+
"error_reason": None,
|
|
317
|
+
"missing_fields": [],
|
|
318
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Shared media URL classification helpers."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from typing import Iterable, List
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _is_http_url(url: str) -> bool:
|
|
10
|
+
lower = (url or "").lower()
|
|
11
|
+
return lower.startswith("http://") or lower.startswith("https://")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def is_probable_image_url(url: str) -> bool:
|
|
15
|
+
lower = (url or "").lower()
|
|
16
|
+
if not _is_http_url(lower):
|
|
17
|
+
return False
|
|
18
|
+
image_tokens = [
|
|
19
|
+
".jpg",
|
|
20
|
+
".jpeg",
|
|
21
|
+
".png",
|
|
22
|
+
".webp",
|
|
23
|
+
".gif",
|
|
24
|
+
"imageview2",
|
|
25
|
+
"imagemogr2",
|
|
26
|
+
"redimage",
|
|
27
|
+
"frame/",
|
|
28
|
+
"sns-img",
|
|
29
|
+
"sns-webpic",
|
|
30
|
+
"notes_pre_post",
|
|
31
|
+
"/image/",
|
|
32
|
+
"/img/",
|
|
33
|
+
]
|
|
34
|
+
return any(token in lower for token in image_tokens)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def is_probable_audio_url(url: str) -> bool:
|
|
38
|
+
lower = (url or "").lower()
|
|
39
|
+
if not _is_http_url(lower):
|
|
40
|
+
return False
|
|
41
|
+
audio_tokens = [
|
|
42
|
+
".m4a",
|
|
43
|
+
".mp3",
|
|
44
|
+
".aac",
|
|
45
|
+
".wav",
|
|
46
|
+
".flac",
|
|
47
|
+
".ogg",
|
|
48
|
+
"/audio/",
|
|
49
|
+
"sns-audio",
|
|
50
|
+
"redaudio",
|
|
51
|
+
]
|
|
52
|
+
return any(token in lower for token in audio_tokens)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def is_probable_video_url(url: str) -> bool:
|
|
56
|
+
lower = (url or "").lower()
|
|
57
|
+
if not _is_http_url(lower):
|
|
58
|
+
return False
|
|
59
|
+
if is_probable_image_url(lower) or is_probable_audio_url(lower):
|
|
60
|
+
return False
|
|
61
|
+
video_tokens = [
|
|
62
|
+
".mp4",
|
|
63
|
+
".m3u8",
|
|
64
|
+
".mov",
|
|
65
|
+
".flv",
|
|
66
|
+
"/video/",
|
|
67
|
+
"sns-video",
|
|
68
|
+
"redvideo",
|
|
69
|
+
"play",
|
|
70
|
+
"stream",
|
|
71
|
+
"master",
|
|
72
|
+
"vod",
|
|
73
|
+
]
|
|
74
|
+
return any(token in lower for token in video_tokens)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def filter_video_urls(urls: Iterable[str]) -> List[str]:
|
|
78
|
+
unique: List[str] = []
|
|
79
|
+
seen = set()
|
|
80
|
+
for raw in urls:
|
|
81
|
+
url = str(raw or "").strip()
|
|
82
|
+
if not url or url in seen or not is_probable_video_url(url):
|
|
83
|
+
continue
|
|
84
|
+
unique.append(url)
|
|
85
|
+
seen.add(url)
|
|
86
|
+
return unique
|
|
@@ -13,6 +13,14 @@ from scripts.pipelines.schema import (
|
|
|
13
13
|
validate_work_item,
|
|
14
14
|
validate_works_collection,
|
|
15
15
|
)
|
|
16
|
+
from scripts.pipelines.douyin_metadata import (
|
|
17
|
+
extract_douyin_author,
|
|
18
|
+
extract_douyin_caption,
|
|
19
|
+
extract_douyin_metrics,
|
|
20
|
+
extract_douyin_title,
|
|
21
|
+
normalize_douyin_author_handle,
|
|
22
|
+
)
|
|
23
|
+
from scripts.pipelines.media_url_rules import is_probable_video_url as is_shared_probable_video_url
|
|
16
24
|
from scripts.core.tikomni_common import deep_find_all, deep_find_first
|
|
17
25
|
from scripts.pipelines.select_low_quality_video_url import select_low_quality_video_url
|
|
18
26
|
|
|
@@ -117,12 +125,7 @@ def _normalize_douyin_tags(value: Any) -> List[str]:
|
|
|
117
125
|
|
|
118
126
|
|
|
119
127
|
def _is_probable_video_url(url: str) -> bool:
|
|
120
|
-
|
|
121
|
-
if not (lower.startswith("http://") or lower.startswith("https://")):
|
|
122
|
-
return False
|
|
123
|
-
if any(token in lower for token in [".jpg", ".jpeg", ".png", ".webp", "image", "img"]):
|
|
124
|
-
return False
|
|
125
|
-
return any(token in lower for token in [".mp4", ".m3u8", ".m4a", "video", "stream", "play"])
|
|
128
|
+
return is_shared_probable_video_url(url)
|
|
126
129
|
|
|
127
130
|
|
|
128
131
|
def _extract_douyin_video_down_url(item: Dict[str, Any]) -> str:
|
|
@@ -348,14 +351,21 @@ def adapt_douyin_author_home(raw: Dict[str, Any]) -> Tuple[Dict[str, Any], List[
|
|
|
348
351
|
|
|
349
352
|
internal_author_id = _t(_first(profile_data, ["sec_user_id", "sec_uid"], raw.get("resolved_author_id")))
|
|
350
353
|
stable_author_id = _t(_first(profile_data, ["uid", "user_id", "id"]))
|
|
351
|
-
author_handle =
|
|
354
|
+
author_handle = normalize_douyin_author_handle(
|
|
355
|
+
_first(profile_data, ["unique_id"]),
|
|
356
|
+
_first(profile_data, ["short_id"]),
|
|
357
|
+
_first(profile_data, ["douyin_id"]),
|
|
358
|
+
_first(profile_data, ["display_id"]),
|
|
359
|
+
_first(profile_data, ["nickname", "name"]),
|
|
360
|
+
)
|
|
361
|
+
nickname = _t(_first(profile_data, ["nickname", "name"]))
|
|
352
362
|
|
|
353
363
|
author_id = internal_author_id or stable_author_id
|
|
354
364
|
profile = build_author_profile(
|
|
355
365
|
platform="douyin",
|
|
356
366
|
platform_author_id=author_id,
|
|
357
367
|
author_handle=author_handle,
|
|
358
|
-
nickname=
|
|
368
|
+
nickname=nickname,
|
|
359
369
|
ip_location=_t(_first(profile_data, ["ip_location", "ip_label", "ipLocation"])),
|
|
360
370
|
fans_count=_i(_first(profile_data, ["follower_count", "fans_count", "mplatform_followers_count"])),
|
|
361
371
|
liked_count=_i(_first(profile_data, ["total_favorited", "liked_count", "favoriting_count"])),
|
|
@@ -383,23 +393,27 @@ def adapt_douyin_author_home(raw: Dict[str, Any]) -> Tuple[Dict[str, Any], List[
|
|
|
383
393
|
if not isinstance(item, dict):
|
|
384
394
|
continue
|
|
385
395
|
aweme_id = _t(_first(item, ["aweme_id", "item_id", "id"]))
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
"comment": _i(_first(item, ["comment_count"], 0)),
|
|
389
|
-
"collect": _i(_first(item, ["collect_count"], 0)),
|
|
390
|
-
"share": _i(_first(item, ["share_count"], 0)),
|
|
391
|
-
"play": _optional_i(_first(item, ["play_count", "view_count"], None)),
|
|
392
|
-
}
|
|
396
|
+
author_info = extract_douyin_author(item)
|
|
397
|
+
metrics = extract_douyin_metrics(item)
|
|
393
398
|
video_down_url = _extract_douyin_video_down_url(item)
|
|
394
399
|
tags = _normalize_douyin_tags(_first(item, ["hashtags", "tags", "text_extra"], []))
|
|
400
|
+
work_author_handle = normalize_douyin_author_handle(
|
|
401
|
+
author_info.get("author_handle"),
|
|
402
|
+
author_handle,
|
|
403
|
+
nickname,
|
|
404
|
+
)
|
|
405
|
+
work_platform_author_id = _t(author_info.get("platform_author_id") or author_id)
|
|
406
|
+
work_author_platform_id = _t(author_info.get("author_platform_id") or stable_author_id or author_id)
|
|
407
|
+
work_nickname = _t(author_info.get("nickname") or nickname)
|
|
408
|
+
work_signature = _t(author_info.get("signature") or profile.get("signature"))
|
|
395
409
|
work = build_work_item(
|
|
396
410
|
platform="douyin",
|
|
397
411
|
platform_work_id=aweme_id,
|
|
398
|
-
platform_author_id=
|
|
399
|
-
author_handle=
|
|
400
|
-
author_platform_id=
|
|
401
|
-
title=
|
|
402
|
-
caption_raw=
|
|
412
|
+
platform_author_id=work_platform_author_id,
|
|
413
|
+
author_handle=work_author_handle,
|
|
414
|
+
author_platform_id=work_author_platform_id,
|
|
415
|
+
title=extract_douyin_title(item),
|
|
416
|
+
caption_raw=extract_douyin_caption(item),
|
|
403
417
|
subtitle_raw="",
|
|
404
418
|
subtitle_source="missing",
|
|
405
419
|
publish_time=_t(_first(item, ["create_time", "publish_time"])),
|
|
@@ -407,7 +421,12 @@ def adapt_douyin_author_home(raw: Dict[str, Any]) -> Tuple[Dict[str, Any], List[
|
|
|
407
421
|
content_type="video",
|
|
408
422
|
duration_ms=_i(_first(item, ["duration_ms", "duration"], 0)),
|
|
409
423
|
tags=tags,
|
|
410
|
-
metrics=
|
|
424
|
+
metrics={
|
|
425
|
+
"digg_count": int(metrics.get("digg_count") or 0),
|
|
426
|
+
"comment_count": int(metrics.get("comment_count") or 0),
|
|
427
|
+
"collect_count": int(metrics.get("collect_count") or 0),
|
|
428
|
+
"share_count": int(metrics.get("share_count") or 0),
|
|
429
|
+
},
|
|
411
430
|
cover_image=(
|
|
412
431
|
_extract_first_url(_first(item, ["cover_url"], ""))
|
|
413
432
|
or _extract_first_url(_first(item, ["cover"], ""))
|
|
@@ -420,18 +439,31 @@ def adapt_douyin_author_home(raw: Dict[str, Any]) -> Tuple[Dict[str, Any], List[
|
|
|
420
439
|
asr_error_reason="",
|
|
421
440
|
asr_source="fallback_none",
|
|
422
441
|
platform_native_refs={
|
|
423
|
-
"douyin_sec_uid": internal_author_id,
|
|
424
|
-
"douyin_aweme_author_id": stable_author_id or author_id,
|
|
442
|
+
"douyin_sec_uid": _t(author_info.get("douyin_sec_uid") or internal_author_id),
|
|
443
|
+
"douyin_aweme_author_id": _t(author_info.get("douyin_aweme_author_id") or stable_author_id or author_id),
|
|
444
|
+
"douyin_unique_id": _t(author_info.get("unique_id")),
|
|
425
445
|
},
|
|
426
446
|
raw_ref={"aweme_id": aweme_id, "raw_item": item},
|
|
427
447
|
)
|
|
428
448
|
work.update(
|
|
429
449
|
{
|
|
430
|
-
"
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
450
|
+
"author": {
|
|
451
|
+
"author_handle": work_author_handle,
|
|
452
|
+
"platform_author_id": work_platform_author_id,
|
|
453
|
+
"author_platform_id": work_author_platform_id,
|
|
454
|
+
"douyin_sec_uid": _t(author_info.get("douyin_sec_uid") or internal_author_id),
|
|
455
|
+
"douyin_aweme_author_id": _t(author_info.get("douyin_aweme_author_id") or stable_author_id or author_id),
|
|
456
|
+
"unique_id": _t(author_info.get("unique_id")),
|
|
457
|
+
"nickname": work_nickname,
|
|
458
|
+
"signature": work_signature,
|
|
459
|
+
},
|
|
460
|
+
"nickname": work_nickname,
|
|
461
|
+
"signature": work_signature,
|
|
462
|
+
"digg_count": int(metrics.get("digg_count") or 0),
|
|
463
|
+
"comment_count": int(metrics.get("comment_count") or 0),
|
|
464
|
+
"collect_count": int(metrics.get("collect_count") or 0),
|
|
465
|
+
"share_count": int(metrics.get("share_count") or 0),
|
|
466
|
+
"play_count": metrics.get("play_count"),
|
|
435
467
|
}
|
|
436
468
|
)
|
|
437
469
|
|
|
@@ -448,16 +480,18 @@ def adapt_xhs_author_home(raw: Dict[str, Any]) -> Tuple[Dict[str, Any], List[Dic
|
|
|
448
480
|
|
|
449
481
|
author_id = _t(_first(profile_data, ["user_id", "userid", "id"], raw.get("resolved_author_id")))
|
|
450
482
|
author_handle = _t(_first(profile_data, ["red_id", "redid", "display_id", "username"]))
|
|
483
|
+
nickname = _t(_first(profile_data, ["nickname", "name"]))
|
|
484
|
+
signature = _t(_first(profile_data, ["desc", "signature", "bio", "introduction"]))
|
|
451
485
|
profile = build_author_profile(
|
|
452
486
|
platform="xiaohongshu",
|
|
453
487
|
platform_author_id=author_id,
|
|
454
488
|
author_handle=author_handle,
|
|
455
|
-
nickname=
|
|
489
|
+
nickname=nickname,
|
|
456
490
|
ip_location=_t(_first(profile_data, ["ip_location", "ip_location_desc", "ipLocation"])),
|
|
457
491
|
fans_count=_i(_first(profile_data, ["fans", "fans_count", "follower_count", "followers"])),
|
|
458
492
|
liked_count=_i(_first(profile_data, ["liked_count", "likes", "total_liked", "like_count"])),
|
|
459
493
|
collected_count=_i(_first(profile_data, ["collected_count", "collect_count", "total_collected", "favorite_count"])),
|
|
460
|
-
signature=
|
|
494
|
+
signature=signature,
|
|
461
495
|
avatar_url=_extract_xhs_avatar_url(profile_data),
|
|
462
496
|
works_count=_i(_first(profile_data, ["notes", "note_count", "works_count", "post_count"])),
|
|
463
497
|
verified=bool(_first(profile_data, ["official_verified", "verified"], False)),
|
|
@@ -480,6 +514,8 @@ def adapt_xhs_author_home(raw: Dict[str, Any]) -> Tuple[Dict[str, Any], List[Dic
|
|
|
480
514
|
"share": _i(_first(item, ["share_count"], 0)),
|
|
481
515
|
"play": _optional_i(_first(item, ["view_count", "play_count"], None)),
|
|
482
516
|
}
|
|
517
|
+
if (metrics["play"] or 0) <= 0 and max(metrics["like"], metrics["comment"], metrics["collect"], metrics["share"]) > 0:
|
|
518
|
+
metrics["play"] = None
|
|
483
519
|
subtitle_inline = _extract_xhs_subtitle_inline(item)
|
|
484
520
|
subtitle_urls = _extract_xhs_subtitle_urls(item)
|
|
485
521
|
video_down_url = _extract_xhs_video_down_url(item)
|
|
@@ -489,6 +525,8 @@ def adapt_xhs_author_home(raw: Dict[str, Any]) -> Tuple[Dict[str, Any], List[Dic
|
|
|
489
525
|
cover_image = _extract_xhs_cover_image(item)
|
|
490
526
|
source_url = _extract_xhs_source_url(item, note_id)
|
|
491
527
|
share_url = _extract_xhs_share_url(item, note_id)
|
|
528
|
+
work_nickname = nickname
|
|
529
|
+
work_signature = signature
|
|
492
530
|
|
|
493
531
|
work = build_work_item(
|
|
494
532
|
platform="xiaohongshu",
|
|
@@ -523,6 +561,15 @@ def adapt_xhs_author_home(raw: Dict[str, Any]) -> Tuple[Dict[str, Any], List[Dic
|
|
|
523
561
|
)
|
|
524
562
|
work.update(
|
|
525
563
|
{
|
|
564
|
+
"author": {
|
|
565
|
+
"author_handle": author_handle,
|
|
566
|
+
"platform_author_id": author_id,
|
|
567
|
+
"author_platform_id": author_id,
|
|
568
|
+
"nickname": work_nickname,
|
|
569
|
+
"signature": work_signature,
|
|
570
|
+
},
|
|
571
|
+
"nickname": work_nickname,
|
|
572
|
+
"signature": work_signature,
|
|
526
573
|
"digg_count": metrics["like"],
|
|
527
574
|
"comment_count": metrics["comment"],
|
|
528
575
|
"collect_count": metrics["collect"],
|