162 lines
3.8 KiB
Go
162 lines
3.8 KiB
Go
package douyin
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
)
|
|
|
|
const routerDataMarker = "window._ROUTER_DATA"
|
|
|
|
// ParseShareTargetHTML extracts only the author identity needed to add a
|
|
// competitor. It intentionally ignores the work's playback and engagement data.
|
|
func ParseShareTargetHTML(body []byte, expectedWorkKey string) (TargetProfile, bool) {
|
|
if len(body) == 0 || len(body) > 2<<20 || !workKeyPattern.MatchString(expectedWorkKey) {
|
|
return TargetProfile{}, false
|
|
}
|
|
root, ok := extractRouterData(body)
|
|
if !ok {
|
|
return TargetProfile{}, false
|
|
}
|
|
loaderData, ok := root["loaderData"].(map[string]any)
|
|
if !ok {
|
|
return TargetProfile{}, false
|
|
}
|
|
page, ok := loaderData["video_(id)/page"].(map[string]any)
|
|
if !ok {
|
|
return TargetProfile{}, false
|
|
}
|
|
videoInfo, ok := page["videoInfoRes"].(map[string]any)
|
|
if !ok {
|
|
return TargetProfile{}, false
|
|
}
|
|
items, ok := videoInfo["item_list"].([]any)
|
|
if !ok || len(items) == 0 {
|
|
return TargetProfile{}, false
|
|
}
|
|
item, ok := items[0].(map[string]any)
|
|
if !ok {
|
|
return TargetProfile{}, false
|
|
}
|
|
if workKey, ok := stringValue(item["aweme_id"]); ok && workKey != expectedWorkKey {
|
|
return TargetProfile{}, false
|
|
}
|
|
author, ok := item["author"].(map[string]any)
|
|
if !ok {
|
|
return TargetProfile{}, false
|
|
}
|
|
return parseShareAuthor(author)
|
|
}
|
|
|
|
func extractRouterData(body []byte) (map[string]any, bool) {
|
|
marker := []byte(routerDataMarker)
|
|
for offset := 0; ; {
|
|
index := bytes.Index(body[offset:], marker)
|
|
if index < 0 {
|
|
return nil, false
|
|
}
|
|
index += offset + len(marker)
|
|
equals := bytes.IndexByte(body[index:], '=')
|
|
if equals < 0 {
|
|
return nil, false
|
|
}
|
|
start := index + equals + 1
|
|
for start < len(body) && (body[start] == ' ' || body[start] == '\n' || body[start] == '\r' || body[start] == '\t') {
|
|
start++
|
|
}
|
|
if start >= len(body) || body[start] != '{' {
|
|
offset = index
|
|
continue
|
|
}
|
|
end, ok := jsonObjectEnd(body, start)
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
var root map[string]any
|
|
if json.Unmarshal(body[start:end], &root) == nil {
|
|
return root, true
|
|
}
|
|
offset = end
|
|
}
|
|
}
|
|
|
|
func jsonObjectEnd(body []byte, start int) (int, bool) {
|
|
depth := 0
|
|
inString := false
|
|
escaped := false
|
|
for index := start; index < len(body); index++ {
|
|
char := body[index]
|
|
if inString {
|
|
if escaped {
|
|
escaped = false
|
|
} else if char == '\\' {
|
|
escaped = true
|
|
} else if char == '"' {
|
|
inString = false
|
|
}
|
|
continue
|
|
}
|
|
switch char {
|
|
case '"':
|
|
inString = true
|
|
case '{':
|
|
depth++
|
|
case '}':
|
|
depth--
|
|
if depth == 0 {
|
|
return index + 1, true
|
|
}
|
|
}
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
func parseShareAuthor(author map[string]any) (TargetProfile, bool) {
|
|
secUID, ok := stringValue(author["sec_uid"])
|
|
if !ok || !keyPattern.MatchString(secUID) {
|
|
return TargetProfile{}, false
|
|
}
|
|
uid := stringOrEmpty(author["uid"])
|
|
uniqueID := stringOrEmpty(author["unique_id"])
|
|
if uid != "" && !keyPattern.MatchString(uid) || uniqueID != "" && !keyPattern.MatchString(uniqueID) {
|
|
return TargetProfile{}, false
|
|
}
|
|
return TargetProfile{
|
|
UID: uid,
|
|
SecUID: secUID,
|
|
UniqueID: uniqueID,
|
|
Nickname: stringOrEmpty(author["nickname"]),
|
|
AvatarURL: avatarURL(author),
|
|
}, true
|
|
}
|
|
|
|
func stringValue(value any) (string, bool) {
|
|
result, ok := value.(string)
|
|
return result, ok && result != ""
|
|
}
|
|
|
|
func stringOrEmpty(value any) string {
|
|
result, _ := value.(string)
|
|
return result
|
|
}
|
|
|
|
func avatarURL(author map[string]any) string {
|
|
for _, key := range []string{"avatar_thumb", "avatar_larger", "avatar_medium", "avatar"} {
|
|
value := author[key]
|
|
if direct, ok := value.(string); ok && direct != "" {
|
|
return direct
|
|
}
|
|
container, ok := value.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
urls, ok := container["url_list"].([]any)
|
|
if !ok || len(urls) == 0 {
|
|
continue
|
|
}
|
|
if result, ok := urls[0].(string); ok {
|
|
return result
|
|
}
|
|
}
|
|
return ""
|
|
}
|