Files

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 ""
}