11
This commit is contained in:
197
scripts/nodejs/publish_to_douyin.js
Normal file
197
scripts/nodejs/publish_to_douyin.js
Normal file
@@ -0,0 +1,197 @@
|
||||
"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 };
|
||||
Reference in New Issue
Block a user