geoGo/internal/publisher/xhssp.go

377 lines
8.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package publisher
import (
"context"
"fmt"
"geo/internal/config"
"log"
"os"
"strings"
"time"
"github.com/go-rod/rod"
"github.com/go-rod/rod/lib/proto"
)
type XiaohongshuVideoPublisher struct {
*BasePublisher
shortWait int
mediumWait int
longWait int
}
func NewXiaohongshuVideoPublisher(ctx context.Context, task *TaskParams, cfg *config.Config, logger *log.Logger) PublisherInerface {
return &XiaohongshuVideoPublisher{
BasePublisher: NewBasePublisher(ctx, task, cfg, logger),
shortWait: 1,
mediumWait: 3,
longWait: 5,
}
}
func (p *XiaohongshuVideoPublisher) CheckLogin() (bool, string) {
p.LogInfo("检查登录状态...")
if err := p.SetupDriver(); err != nil {
return false, fmt.Sprintf("浏览器启动失败: %v", err)
}
defer p.Page.Close()
p.Page.MustNavigate(p.LoginedURL)
p.Sleep(3)
p.WaitForPageReady(5)
if p.CheckLoginStatus() {
p.SaveCookies()
return true, "已登录"
}
return false, "未登录"
}
func (p *XiaohongshuVideoPublisher) WaitLogin() (bool, string) {
p.LogInfo("开始等待登录...")
if err := p.SetupDriver(); err != nil {
return false, fmt.Sprintf("浏览器启动失败: %v", err)
}
defer p.Close()
p.Page.MustNavigate(p.LoginedURL)
p.Sleep(3)
if p.CheckLoginStatus() {
p.SaveCookies()
return true, "already_logged_in"
}
startTime := time.Now()
timeout := 120
for time.Since(startTime) < time.Duration(timeout)*time.Second {
if p.CheckLoginStatus() {
p.SaveCookies()
return true, "login_success"
}
p.SleepMs(1000)
}
return false, "登录超时,请检查网络或账号状态"
}
func (p *XiaohongshuVideoPublisher) waitForEditorReady(timeout int) bool {
p.LogInfo("等待编辑器加载...")
startTime := time.Now()
for time.Since(startTime) < time.Duration(timeout)*time.Second {
uploadArea, err := p.Page.Element(".upload-wrapper")
if err == nil && uploadArea != nil {
p.LogInfo("编辑器加载完成")
return true
}
p.SleepMs(1000)
}
p.LogInfo("编辑器加载超时")
return false
}
func (p *XiaohongshuVideoPublisher) uploadVideo() error {
if p.SourcePath == "" {
return fmt.Errorf("视频不存在")
}
if _, err := os.Stat(p.SourcePath); os.IsNotExist(err) {
return fmt.Errorf("视频不存在")
}
p.LogInfo(fmt.Sprintf("开始上传视频: %s", p.SourcePath))
fileInputSelectors := []string{
"input[type='file'][accept*='video']",
".upload-input",
"input[accept*='mp4']",
"input[accept*='video/*']",
}
fileInput, err := p.Page.Element(".upload-input")
if err != nil {
fmt.Errorf("找到文件上传输入框")
}
if fileInput == nil {
uploadArea, err := p.WaitForElementVisible(".video-plugin-title-action", 5)
if err == nil && uploadArea != nil {
p.LogInfo("点击上传区域")
p.JSClick(uploadArea)
p.SleepMs(1000)
for _, selector := range fileInputSelectors {
fileInput, _ = p.Page.Element(selector)
if fileInput != nil {
break
}
}
}
}
if fileInput == nil {
return fmt.Errorf("未找到文件上传输入框")
}
if err := fileInput.SetFiles([]string{p.SourcePath}); err != nil {
return fmt.Errorf("上传视频失败: %v", err)
}
p.LogInfo(fmt.Sprintf("视频文件已选择: %s", p.SourcePath))
return p.waitForUploadComplete()
}
func (p *XiaohongshuVideoPublisher) waitForUploadComplete() error {
p.LogInfo("等待视频上传完成...")
for i := 0; i < 300; i++ {
publishBtn, err := p.Page.Element(".publish-page-publish-btn button.bg-red")
if err == nil && publishBtn != nil {
text, _ := publishBtn.Text()
if text == "发布" {
p.LogInfo("发布按钮已可点击,视频上传完成")
return nil
}
}
errorElem, err := p.Page.Element("[class*='error'], .toast-error")
if err == nil && errorElem != nil {
visible, _ := errorElem.Visible()
if visible {
text, _ := errorElem.Text()
if text != "" {
return fmt.Errorf("上传失败: %s", text)
}
}
}
p.SleepMs(1000)
}
return fmt.Errorf("上传超时")
}
func (p *XiaohongshuVideoPublisher) inputTitle() error {
p.LogInfo(fmt.Sprintf("输入视频标题: %s", p.Title))
exist, titleInput, err := p.Page.Has("input[placeholder*='标题']")
if err != nil {
return err
}
if !exist {
return fmt.Errorf("未找到标题输入框")
}
p.ClearInput(titleInput)
p.SleepMs(300)
titleInput.Input("")
p.SleepMs(300)
titleInput.Input(p.Title)
p.LogInfo(fmt.Sprintf("标题已输入: %s", p.Title))
p.SleepMs(500)
return nil
}
func (p *XiaohongshuVideoPublisher) inputDescription() error {
p.LogInfo("输入视频描述...")
fullDescription := p.Content
if len(p.Tags) > 0 {
tagStr := ""
for _, tag := range p.Tags {
if tag != "" {
tagStr += fmt.Sprintf("#%s ", tag)
}
}
tagStr = strings.TrimSpace(tagStr)
if fullDescription != "" {
fullDescription = fmt.Sprintf("%s\n\n%s", tagStr, fullDescription)
} else {
fullDescription = tagStr
}
}
p.LogInfo(fmt.Sprintf("描述内容: %s...", fullDescription[:min(len(fullDescription), 100)]))
editorSelectors := []string{
".tiptap.ProseMirror",
".ProseMirror",
"[contenteditable='true']",
".editor-content .tiptap",
}
for _, selector := range editorSelectors {
editor, err := p.WaitForElementVisible(selector, 5)
if err == nil && editor != nil {
p.LogInfo(fmt.Sprintf("找到编辑器: %s", selector))
editor.Click(proto.InputMouseButtonLeft, 1)
p.SleepMs(500)
p.SetContentEditable(editor, fullDescription)
p.SleepMs(1000)
return nil
}
}
return fmt.Errorf("未找到描述输入框")
}
func (p *XiaohongshuVideoPublisher) clickPublish() error {
p.LogInfo("点击发布按钮...")
p.Page.Eval(`() => window.scrollTo(0, document.body.scrollHeight)`)
p.SleepMs(1000)
var publishBtn *rod.Element
publishContainer, err := p.WaitForElementVisible(".publish-page-publish-btn", 5)
if err == nil && publishContainer != nil {
buttons, _ := publishContainer.Elements("button")
for _, btn := range buttons {
text, _ := btn.Text()
if text == "发布" {
publishBtn = btn
p.LogInfo("找到发布按钮")
break
}
}
}
if publishBtn == nil {
allButtons, _ := p.Page.Elements("button")
for _, btn := range allButtons {
text, _ := btn.Text()
if text == "发布" && !strings.Contains(text, "暂存") {
publishBtn = btn
p.LogInfo("通过遍历找到发布按钮")
break
}
}
}
if publishBtn == nil {
return fmt.Errorf("未找到发布按钮")
}
publishBtn.ScrollIntoView()
p.SleepMs(500)
if err := publishBtn.Click(proto.InputMouseButtonLeft, 1); err != nil {
p.JSClick(publishBtn)
p.LogInfo("使用JS点击发布按钮")
} else {
p.LogInfo("已点击发布按钮")
}
p.SleepMs(2000)
return nil
}
func (p *XiaohongshuVideoPublisher) waitForPublishResult(timeout int) (bool, string) {
p.LogInfo("等待发布结果...")
startTime := time.Now()
for time.Since(startTime) < time.Duration(timeout)*time.Second {
currentURL := p.GetCurrentURL()
successKeywords := []string{"success", "content/manage", "work-management"}
for _, keyword := range successKeywords {
if strings.Contains(currentURL, keyword) {
p.LogInfo(fmt.Sprintf("发布成功URL: %s", currentURL))
return true, "发布成功"
}
}
toasts, _ := p.Page.Elements(".semi-toast-content, [class*='toast']")
for _, toast := range toasts {
visible, _ := toast.Visible()
if visible {
text, _ := toast.Text()
if strings.Contains(text, "成功") || strings.Contains(text, "已发布") {
p.LogInfo(fmt.Sprintf("发布成功: %s", text))
return true, text
} else if strings.Contains(text, "失败") {
p.LogError(fmt.Sprintf("发布失败: %s", text))
return false, text
}
}
}
if strings.Contains(currentURL, "publish") && time.Since(startTime) > 10*time.Second {
errorMsgs, _ := p.Page.Elements("[class*='error'], .toast-error")
for _, elem := range errorMsgs {
visible, _ := elem.Visible()
if visible {
text, _ := elem.Text()
if text != "" {
return false, text
}
}
}
}
p.SleepMs(2000)
}
return false, "发布结果未知(超时)"
}
func (p *XiaohongshuVideoPublisher) PublishNote() (bool, string) {
p.StartNote()
if err := p.SetupDriver(); err != nil {
return false, fmt.Sprintf("浏览器启动失败: %v", err)
}
defer p.Close()
steps := []struct {
name string
fn func() error
}{
{"初始化页面", p.InitPage},
{"上传视频", p.uploadVideo},
{"输入标题", p.inputTitle},
{"输入描述", p.inputDescription},
{"点击发布", p.clickPublish},
}
for _, step := range steps {
if err := step.fn(); err != nil {
p.LogStep(step.name, false, err.Error())
return false, fmt.Sprintf("%s失败: %v", step.name, err)
}
p.LogStep(step.name, true, "")
p.SleepMs(1000)
}
success, message := p.waitForPublishResult(60)
if success {
p.LogInfo(fmt.Sprintf("🎉 发布成功!%s", message))
return true, message
}
p.LogError(fmt.Sprintf("发布失败: %s", message))
return false, message
}