198 lines
5.5 KiB
JavaScript
198 lines
5.5 KiB
JavaScript
"use strict";
|
||
|
||
/**
|
||
* 抖音创作者中心发布(对齐 Electron publishToDouyin,Puppeteer 版)
|
||
*/
|
||
const fs = require("fs");
|
||
const { delay } = require("./publish_browser.js");
|
||
|
||
async function clickIfVisible(page, selector) {
|
||
const el = await page.$(selector);
|
||
if (!el) return false;
|
||
try {
|
||
await el.click();
|
||
return true;
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
async function typeIntoFirst(page, selectors, text) {
|
||
for (const sel of selectors) {
|
||
const el = await page.$(sel);
|
||
if (!el) continue;
|
||
try {
|
||
await el.click({ clickCount: 3 });
|
||
await page.keyboard.down("Control");
|
||
await page.keyboard.press("a");
|
||
await page.keyboard.up("Control");
|
||
await page.keyboard.press("Backspace");
|
||
await el.type(text, { delay: 30 });
|
||
return true;
|
||
} catch {
|
||
/* try next */
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
async function publishToDouyin(page, params) {
|
||
const { videoPath, coverPath, title, tags = [], autoPublish } = params;
|
||
if (!fs.existsSync(videoPath)) throw new Error("视频文件不存在");
|
||
if (!fs.existsSync(coverPath)) throw new Error("封面文件不存在");
|
||
|
||
globalThis.__native?.emitProgress?.("正在打开抖音上传页…");
|
||
const uploadUrl = "https://creator.douyin.com/creator-micro/content/upload";
|
||
await page.goto(uploadUrl, { waitUntil: "domcontentloaded" });
|
||
await delay(3000);
|
||
|
||
let currentUrl = page.url();
|
||
if (currentUrl.includes("login") || currentUrl.includes("passport")) {
|
||
throw new Error("未登录抖音,请先在步骤 07 点击「登录平台」");
|
||
}
|
||
|
||
let avatar = await page.$("#header-avatar");
|
||
if (!avatar) {
|
||
await delay(5000);
|
||
avatar = await page.$("#header-avatar");
|
||
}
|
||
if (!avatar) {
|
||
throw new Error("抖音登录已过期,请重新登录");
|
||
}
|
||
|
||
if (!page.url().includes("content/upload")) {
|
||
await page.goto(uploadUrl, { waitUntil: "domcontentloaded" });
|
||
await delay(3000);
|
||
}
|
||
|
||
globalThis.__native?.emitProgress?.("正在上传视频…");
|
||
await page.waitForSelector('input[type="file"]', { timeout: 60000 });
|
||
const fileInputs = await page.$$('input[type="file"]');
|
||
if (!fileInputs.length) throw new Error("未找到视频上传控件");
|
||
await fileInputs[0].uploadFile(videoPath);
|
||
await delay(5000);
|
||
|
||
globalThis.__native?.emitProgress?.("等待视频处理…");
|
||
const start = Date.now();
|
||
let ready = false;
|
||
while (Date.now() - start < 300000 && !ready) {
|
||
const titleBox = await page.$('textarea[placeholder*="标题"], input[placeholder*="标题"]');
|
||
if (titleBox) {
|
||
try {
|
||
const disabled = await page.evaluate((el) => el.disabled, titleBox);
|
||
if (!disabled) ready = true;
|
||
} catch {
|
||
ready = true;
|
||
}
|
||
}
|
||
if (!ready) await delay(2000);
|
||
}
|
||
|
||
globalThis.__native?.emitProgress?.("正在上传封面…");
|
||
try {
|
||
await clickIfVisible(page, 'button');
|
||
const buttons = await page.$$("button");
|
||
for (const btn of buttons) {
|
||
const text = await page.evaluate((el) => el.textContent || "", btn);
|
||
if (String(text).includes("选择封面")) {
|
||
await btn.click();
|
||
await delay(2000);
|
||
break;
|
||
}
|
||
}
|
||
const imgInput = await page.$('input[type="file"][accept*="image"]');
|
||
if (imgInput) {
|
||
await imgInput.uploadFile(coverPath);
|
||
await delay(3000);
|
||
}
|
||
const doneBtns = await page.$$("button");
|
||
for (const btn of doneBtns) {
|
||
const text = await page.evaluate((el) => el.textContent || "", btn);
|
||
if (String(text).trim() === "完成") {
|
||
await btn.click();
|
||
await delay(2000);
|
||
break;
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.warn("封面上传跳过:", e.message);
|
||
}
|
||
|
||
globalThis.__native?.emitProgress?.("正在填写标题与话题…");
|
||
const titleText = String(title || "").slice(0, 30);
|
||
await typeIntoFirst(
|
||
page,
|
||
[
|
||
'input[placeholder*="填写作品标题"]',
|
||
'input[placeholder*="标题"]',
|
||
'textarea[placeholder*="标题"]',
|
||
],
|
||
titleText,
|
||
);
|
||
|
||
const tagList = Array.isArray(tags) ? tags : [];
|
||
if (tagList.length) {
|
||
const descOk = await typeIntoFirst(
|
||
page,
|
||
[
|
||
'textarea[placeholder*="描述"]',
|
||
'textarea[placeholder*="简介"]',
|
||
'motion[contenteditable="true"]',
|
||
'div[contenteditable="true"]',
|
||
],
|
||
"",
|
||
);
|
||
if (descOk) {
|
||
for (const tag of tagList) {
|
||
const t = String(tag).trim();
|
||
if (!t) continue;
|
||
const clean = t.startsWith("#") ? t : `#${t}`;
|
||
await page.keyboard.type(clean, { delay: 40 });
|
||
await page.keyboard.press("Space");
|
||
await delay(400);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!autoPublish) {
|
||
return {
|
||
success: false,
|
||
status: "manual_pending",
|
||
message: "内容已填写,请在浏览器中检查并手动点击发布",
|
||
videoUrl: page.url(),
|
||
keepPageOpen: true,
|
||
};
|
||
}
|
||
|
||
globalThis.__native?.emitProgress?.("正在点击发布…");
|
||
const publishBtns = await page.$$("button");
|
||
let clicked = false;
|
||
for (const btn of publishBtns) {
|
||
const text = (await page.evaluate((el) => el.textContent || "", btn)).trim();
|
||
if (text === "发布" || text === "确定发布") {
|
||
await btn.click();
|
||
clicked = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (!clicked) {
|
||
return {
|
||
success: false,
|
||
status: "manual_pending",
|
||
message: "未找到发布按钮,请手动发布",
|
||
videoUrl: page.url(),
|
||
};
|
||
}
|
||
|
||
await delay(5000);
|
||
return {
|
||
success: true,
|
||
status: "success",
|
||
message: "已提交发布",
|
||
videoUrl: page.url(),
|
||
};
|
||
}
|
||
|
||
module.exports = { publishToDouyin };
|