387 lines
9.3 KiB
Go
387 lines
9.3 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 DouyinSpPublisher struct {
|
||
*BasePublisher
|
||
}
|
||
|
||
func NewDouyinSpPublisher(ctx context.Context, task *TaskParams, cfg *config.Config, logger *log.Logger) PublisherInerface {
|
||
return &DouyinSpPublisher{NewBasePublisher(ctx, task, cfg, logger)}
|
||
}
|
||
|
||
func (p *DouyinSpPublisher) CheckLoginStatus() bool {
|
||
currentURL := p.GetCurrentURL()
|
||
if strings.Contains(currentURL, p.LoginedURL) {
|
||
return true
|
||
}
|
||
if strings.Contains(currentURL, p.EditorURL) {
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
func (p *DouyinSpPublisher) WaitLogin() (bool, string) {
|
||
p.LogInfo("开始等待登录...")
|
||
|
||
if err := p.SetupDriver(); err != nil {
|
||
return false, fmt.Sprintf("浏览器启动失败: %v", err)
|
||
}
|
||
defer p.Close()
|
||
|
||
p.Page.MustNavigate(p.LoginURL)
|
||
p.Sleep(2)
|
||
|
||
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 *DouyinSpPublisher) waitForEditorReady(timeout int) bool {
|
||
p.LogInfo("等待编辑器加载...")
|
||
startTime := time.Now()
|
||
for time.Since(startTime) < time.Duration(timeout)*time.Second {
|
||
uploadArea, err := p.Page.Element(".container-drag-icon")
|
||
if err == nil && uploadArea != nil {
|
||
p.LogInfo("编辑器加载完成")
|
||
return true
|
||
}
|
||
p.SleepMs(1000)
|
||
}
|
||
p.LogInfo("编辑器加载超时")
|
||
return false
|
||
}
|
||
|
||
func (p *DouyinSpPublisher) 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']",
|
||
".container-drag-VAfIfu input[type='file']",
|
||
"input[accept*='video']",
|
||
}
|
||
|
||
var fileInput *rod.Element
|
||
for _, selector := range fileInputSelectors {
|
||
fileInput, _ = p.Page.Element(selector)
|
||
if fileInput != nil {
|
||
p.LogInfo(fmt.Sprintf("找到文件上传输入框: %s", selector))
|
||
break
|
||
}
|
||
}
|
||
|
||
if fileInput == nil {
|
||
uploadArea, err := p.WaitForElementVisible(".container-drag-VAfIfu", 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 *DouyinSpPublisher) waitForUploadComplete() error {
|
||
p.LogInfo("等待视频上传完成...")
|
||
|
||
for i := 0; i < 300; i++ {
|
||
successElements, _ := p.Page.Elements(".upload-success, [class*='success']")
|
||
if len(successElements) > 0 {
|
||
p.LogInfo("视频上传成功")
|
||
p.SleepMs(2000)
|
||
return nil
|
||
}
|
||
|
||
errorElements, _ := p.Page.Elements(".upload-error, [class*='error']")
|
||
for _, elem := range errorElements {
|
||
visible, _ := elem.Visible()
|
||
if visible {
|
||
text, _ := elem.Text()
|
||
if text != "" {
|
||
return fmt.Errorf("上传失败: %s", text)
|
||
}
|
||
}
|
||
}
|
||
|
||
p.SleepMs(1000)
|
||
}
|
||
|
||
return fmt.Errorf("上传超时")
|
||
}
|
||
|
||
func (p *DouyinSpPublisher) inputTitle() error {
|
||
p.LogInfo("输入视频标题...")
|
||
|
||
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 *DouyinSpPublisher) inputDescription() error {
|
||
p.LogInfo("输入视频描述...")
|
||
|
||
descSelectors := []string{
|
||
".editor-kit-container",
|
||
".ProseMirror",
|
||
"[contenteditable='true']",
|
||
}
|
||
|
||
for _, selector := range descSelectors {
|
||
descInput, err := p.WaitForElementVisible(selector, 5)
|
||
if err == nil && descInput != nil {
|
||
p.LogInfo(fmt.Sprintf("找到描述输入框: %s", selector))
|
||
|
||
fullDescription := ""
|
||
for _, tag := range p.Tags {
|
||
fullDescription += fmt.Sprintf("#%s ", tag)
|
||
}
|
||
fullDescription = strings.TrimSpace(fullDescription)
|
||
|
||
p.SetContentEditable(descInput, fullDescription)
|
||
p.SleepMs(3000)
|
||
return nil
|
||
}
|
||
}
|
||
|
||
return fmt.Errorf("未找到描述输入框")
|
||
}
|
||
|
||
func (p *DouyinSpPublisher) clickPublish() error {
|
||
p.LogInfo("点击发布按钮...")
|
||
|
||
p.Page.Eval(`() => window.scrollTo(0, document.body.scrollHeight)`)
|
||
p.SleepMs(2000)
|
||
|
||
var publishBtn *rod.Element
|
||
|
||
popoverSpan, err := p.WaitForElementVisible("#popover-tip-container", 5)
|
||
if err == nil && popoverSpan != nil {
|
||
publishBtn, _ = popoverSpan.Element("button")
|
||
if publishBtn != nil {
|
||
text, _ := publishBtn.Text()
|
||
if text == "发布" {
|
||
p.LogInfo("通过 popover-tip-container 找到发布按钮")
|
||
}
|
||
}
|
||
}
|
||
|
||
if publishBtn == nil {
|
||
publishSelectors := []string{
|
||
"button:contains('发布')",
|
||
"button.primary-cECiOJ",
|
||
"button.primary_button",
|
||
"button[class*='primary']",
|
||
}
|
||
for _, selector := range publishSelectors {
|
||
publishBtn, _ = p.WaitForElementClickable(selector, 3)
|
||
if publishBtn != nil {
|
||
p.LogInfo(fmt.Sprintf("找到发布按钮: %s", selector))
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
if publishBtn == nil {
|
||
p.Page.Eval(`() => window.scrollTo(0, document.body.scrollHeight)`)
|
||
p.SleepMs(1000)
|
||
allButtons, _ := p.Page.Elements("button")
|
||
for _, btn := range allButtons {
|
||
text, _ := btn.Text()
|
||
if text == "发布" {
|
||
publishBtn = btn
|
||
p.LogInfo("通过遍历按钮找到发布按钮")
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
if publishBtn == nil {
|
||
return fmt.Errorf("未找到发布按钮")
|
||
}
|
||
|
||
p.SleepMs(500)
|
||
|
||
if err := publishBtn.Click(proto.InputMouseButtonLeft, 1); err != nil {
|
||
if err := p.JSClick(publishBtn); err != nil {
|
||
return fmt.Errorf("点击发布按钮失败: %v", err)
|
||
}
|
||
}
|
||
|
||
p.LogInfo("已点击发布按钮")
|
||
p.SleepMs(3000)
|
||
|
||
return nil
|
||
}
|
||
|
||
func (p *DouyinSpPublisher) waitForPublishResult(timeout int) (bool, string) {
|
||
p.LogInfo("等待发布结果...")
|
||
|
||
startTime := time.Now()
|
||
for time.Since(startTime) < time.Duration(timeout)*time.Second {
|
||
currentURL := p.GetCurrentURL()
|
||
|
||
successKeywords := []string{"content/manage", "work-management", "success"}
|
||
for _, keyword := range successKeywords {
|
||
if strings.Contains(currentURL, keyword) {
|
||
p.LogInfo(fmt.Sprintf("发布成功!URL: %s", currentURL))
|
||
return true, "发布成功"
|
||
}
|
||
}
|
||
|
||
successSelectors := []string{".semi-toast-content", ".toast-success", "[class*='success']"}
|
||
for _, selector := range successSelectors {
|
||
msgs, _ := p.Page.Elements(selector)
|
||
for _, elem := range msgs {
|
||
visible, _ := elem.Visible()
|
||
if visible {
|
||
text, _ := elem.Text()
|
||
if strings.Contains(text, "成功") || strings.Contains(text, "已发布") {
|
||
p.LogInfo(fmt.Sprintf("发布成功: %s", text))
|
||
return true, text
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
errorSelectors := []string{".semi-toast-content", ".toast-error", "[class*='error']"}
|
||
for _, selector := range errorSelectors {
|
||
msgs, _ := p.Page.Elements(selector)
|
||
for _, elem := range msgs {
|
||
visible, _ := elem.Visible()
|
||
if visible {
|
||
text, _ := elem.Text()
|
||
if strings.Contains(text, "失败") || strings.Contains(strings.ToLower(text), "error") {
|
||
p.LogError(fmt.Sprintf("发布失败: %s", text))
|
||
return false, text
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
p.SleepMs(1000)
|
||
}
|
||
|
||
return false, "发布结果未知(超时)"
|
||
}
|
||
|
||
// InitPage 初始化页面
|
||
func (p *DouyinSpPublisher) InitPage() error {
|
||
|
||
// 尝试加载cookies并检查登录状态
|
||
if err := p.LoadCookies(); err == nil {
|
||
p.Page.MustNavigate(p.LoginURL)
|
||
p.WaitForPageReady(5)
|
||
p.Sleep(4)
|
||
}
|
||
// 统一检查登录状态
|
||
if !p.CheckLoginStatus() {
|
||
p.LogInfo("未登录或登录已过期,需要重新登录")
|
||
p.DelCookies()
|
||
return fmt.Errorf("需要登录")
|
||
}
|
||
p.Page.MustNavigate(p.EditorURL)
|
||
p.WaitForPageReady(5)
|
||
p.SaveCookies()
|
||
return nil
|
||
}
|
||
|
||
func (p *DouyinSpPublisher) 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, "")
|
||
}
|
||
|
||
success, message := p.waitForPublishResult(120)
|
||
if success {
|
||
p.LogInfo(fmt.Sprintf("🎉 发布成功!%s", message))
|
||
return true, message
|
||
}
|
||
p.LogError(fmt.Sprintf("发布失败: %s", message))
|
||
return false, message
|
||
}
|
||
|
||
func (p *DouyinSpPublisher) LogWarning(message string) {
|
||
p.Logger.Printf("⚠️ %s", message)
|
||
}
|