Files
EveryPublish/client/core/internal/exec/cdp/publish.go
T

145 lines
4.2 KiB
Go

// Publish 通用发布流程:进上传页 → 注入 cookie → 传视频 → 填表单 → 点发布 → 等成功跳转 → 截图回执。
// 各平台选择器差异经 PublishConfig 配置;平台改版时只需调整配置。
package cdp
import (
"context"
"fmt"
"strings"
"time"
"github.com/go-rod/rod"
"github.com/go-rod/rod/lib/proto"
)
// PublishConfig 发布流程配置(选择器按平台填写)
type PublishConfig struct {
Platform string
UploadURL string
SuccessMarker string // 发布成功后的 URL 特征
FileInputSel string
TitleSel string
DescSel string
PublishBtnSel string
FormWait time.Duration // 等视频处理出表单的最长时间
SuccessWait time.Duration
ReceiptDir string
}
// PublishResult 发布结果
type PublishResult struct {
URL string
Receipts []string
}
// Publish 执行通用发布流程
func (b *Browser) Publish(ctx context.Context, cfg PublishConfig, videoPath, title, content string, tags []string, cookieStr, cookieDomain string) (*PublishResult, error) {
if err := b.Page.Navigate(cfg.UploadURL); err != nil {
return nil, err
}
_ = b.Page.WaitLoad()
// 注入登录 cookie(有则免扫码)
if cookieStr != "" {
_ = b.InjectCookies(cookieStr, cookieDomain)
if err := b.Page.Navigate(cfg.UploadURL); err != nil {
return nil, err
}
_ = b.Page.WaitLoad()
}
// 1) 传视频
fileInput, err := b.Page.Element(cfg.FileInputSel)
if err != nil {
return nil, fmt.Errorf("找不到上传入口(%s): %w", cfg.FileInputSel, err)
}
if err = fileInput.SetFiles([]string{videoPath}); err != nil {
return nil, fmt.Errorf("选择文件失败: %w", err)
}
Log("%s: 视频已选择,等待平台处理...", cfg.Platform)
// 2) 等表单渲染(视频转码后出现标题输入框)
titleEl, err := b.Page.ElementR(cfg.TitleSel, "")
if err != nil {
titleEl, err = b.waitElement(ctx, cfg.TitleSel, cfg.FormWait)
}
if err != nil {
b.Screenshot(cfg.ReceiptDir, cfg.Platform+"-form-fail.png")
return nil, fmt.Errorf("等表单渲染超时(%s): %w", cfg.TitleSel, err)
}
// 3) 填标题/描述(话题并入描述)
if err = titleEl.Input(title); err != nil {
return nil, fmt.Errorf("填标题失败: %w", err)
}
desc := content
if len(tags) > 0 {
for _, t := range tags {
t = strings.TrimSpace(t)
if t != "" && !strings.HasPrefix(t, "#") {
t = "#" + t
}
desc += " " + t
}
}
if descEl, derr := b.Page.Element(cfg.DescSel); derr == nil {
_ = descEl.Input(desc)
}
// 4) 点发布
btn, err := b.waitElement(ctx, cfg.PublishBtnSel, 15*time.Second)
if err != nil {
b.Screenshot(cfg.ReceiptDir, cfg.Platform+"-btn-fail.png")
return nil, fmt.Errorf("找不到发布按钮(%s): %w", cfg.PublishBtnSel, err)
}
if err = btn.Click(proto.InputMouseButtonLeft, 1); err != nil {
return nil, fmt.Errorf("点发布失败: %w", err)
}
Log("%s: 已点发布,等待结果...", cfg.Platform)
// 5) 等成功跳转
if err = b.waitURL(ctx, cfg.SuccessMarker, cfg.SuccessWait); err != nil {
b.Screenshot(cfg.ReceiptDir, cfg.Platform+"-publish-fail.png")
return nil, err
}
info, _ := b.Page.Info()
receipt := b.Screenshot(cfg.ReceiptDir, cfg.Platform+"-success.png")
Log("%s: 发布成功 %s", cfg.Platform, info.URL)
return &PublishResult{URL: info.URL, Receipts: []string{receipt}}, nil
}
// waitElement 轮询等待元素出现
func (b *Browser) waitElement(ctx context.Context, selector string, timeout time.Duration) (*rod.Element, error) {
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
select {
case <-ctx.Done():
return nil, fmt.Errorf("cancelled")
default:
}
el, err := b.Page.Element(selector)
if err == nil {
return el, nil
}
time.Sleep(2 * time.Second)
}
return nil, fmt.Errorf("timeout waiting %s", selector)
}
// waitURL 轮询等待 URL 命中特征
func (b *Browser) waitURL(ctx context.Context, marker string, timeout time.Duration) error {
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
select {
case <-ctx.Done():
return fmt.Errorf("cancelled")
default:
}
info, err := b.Page.Info()
if err == nil && strings.Contains(info.URL, marker) {
return nil
}
time.Sleep(2 * time.Second)
}
return fmt.Errorf("timeout waiting url %s", marker)
}