377 lines
8.9 KiB
Go
377 lines
8.9 KiB
Go
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
|
||
}
|