Skip to content

Commit d8b6f3b

Browse files
authored
185 feat support video cover upload (#186)
* feat(publish): add cover image support in video data processing - Updated VideoData interface to include optional cover property. - Modified processVideo function to handle cover image processing alongside video. * feat(douyin): implement cover image upload functionality - Added uploadCover function to handle cover image uploads. - Updated VideoDouyin function to process cover image alongside video data. - Enhanced tag handling by limiting the number of tags synced to five. * feat(tiktok): add cover image upload functionality - Implemented uploadCover function to handle cover image uploads. - Enhanced VideoTiktok function to process cover image alongside video data. - Added error handling for missing input elements during upload. * feat(bilibili): add cover image upload functionality - Implemented uploadCover function to handle cover image uploads. - Updated VideoBilibili function to process cover image alongside video data. - Enhanced error handling for missing input elements during upload. * feat(rednote): add cover image upload functionality - Implemented uploadCover function to handle cover image uploads. - Updated VideoRednote function to process cover image alongside video data. - Enhanced error handling for missing input elements during upload. * feat(kuaishou): add cover image upload functionality - Implemented uploadCover function to handle cover image uploads. - Updated VideoKuaishou function to process cover image alongside video data. - Enhanced error handling for missing input elements during upload. * feat(youtube): implement cover image upload functionality - Added uploadCover function to handle cover image uploads. - Enhanced VideoYoutube function to process cover image alongside video data. - Improved error handling for missing input elements during upload. * feat(kuaishou): refine cover image upload handling * feat(youtube): refactor waitForElement and uploadCover functions - Moved waitForElement function inside VideoYoutube for better encapsulation. - Improved error handling and logging for element selection and cover upload process. - Ensured cover file upload events are dispatched correctly. * feat(rednote): improve cover image upload process - Refactored uploadCover function for better element selection and error handling. - Updated cover upload logic to use more specific selectors and improved debugging output. - Ensured cover file is uploaded correctly and confirmed with the appropriate button. * feat(weixinchannel): integrate cover image upload in video processing - Added uploadCover function to handle cover image uploads. - Updated VideoWeiXinChannel function to process cover image alongside video data. - Improved error handling and debugging for cover upload process. * feat(baijiahao): add cover image upload functionality - Implemented uploadCover function to handle cover image uploads. - Updated VideoBaijiahao function to process cover image alongside video data. - Improved error handling and debugging for cover upload process. * feat(weibo): add cover image upload functionality - Implemented uploadCover function to handle cover image uploads. - Updated VideoWeibo function to process cover image alongside video data. - Improved error handling and debugging for cover upload process.
1 parent 4ad4cbc commit d8b6f3b

File tree

11 files changed

+675
-69
lines changed

11 files changed

+675
-69
lines changed

src/sync/common.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export interface VideoData {
5656
content: string;
5757
video: FileData;
5858
tags?: string[];
59+
cover?: FileData;
5960
}
6061

6162
export interface PlatformInfo {

src/sync/video/baijiahao.ts

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,71 @@ export async function VideoBaijiahao(data: SyncData) {
7070
});
7171
}
7272

73+
async function uploadCover(cover: { url: string; name: string; type?: string }) {
74+
console.log('tryCover', cover);
75+
76+
// 1. Find and click the cover upload button
77+
const coverUploadContainer = document.querySelector(
78+
'div.cheetah-upload span.cheetah-upload div.cheetah-spin-container',
79+
);
80+
console.log('coverUpload', coverUploadContainer);
81+
if (!coverUploadContainer) return;
82+
83+
const coverUploadButton = coverUploadContainer.firstChild as HTMLElement;
84+
console.log('coverUploadButton', coverUploadButton);
85+
if (!coverUploadButton) return;
86+
87+
coverUploadButton.click();
88+
await new Promise((resolve) => setTimeout(resolve, 1000));
89+
90+
// 2. Find the file input
91+
const fileInput = document.querySelector("div.cheetah-tabs-content input[name='media']") as HTMLInputElement;
92+
console.log('fileInput', fileInput);
93+
if (!fileInput) return;
94+
95+
// 3. Prepare the file for upload
96+
const dataTransfer = new DataTransfer();
97+
98+
console.log('try upload file', cover);
99+
if (!cover.type || !cover.type.includes('image/')) {
100+
console.log('Cover is not an image, skipping upload');
101+
return;
102+
}
103+
104+
const response = await fetch(cover.url);
105+
const arrayBuffer = await response.arrayBuffer();
106+
const coverFile = new File([arrayBuffer], cover.name, { type: cover.type });
107+
108+
dataTransfer.items.add(coverFile);
109+
110+
if (dataTransfer.files.length === 0) return;
111+
112+
// 4. Set the file on the input and dispatch events
113+
fileInput.files = dataTransfer.files;
114+
115+
const changeEvent = new Event('change', { bubbles: true });
116+
fileInput.dispatchEvent(changeEvent);
117+
118+
const inputEvent = new Event('input', { bubbles: true });
119+
fileInput.dispatchEvent(inputEvent);
120+
121+
console.log('文件上传操作触发');
122+
await new Promise((resolve) => setTimeout(resolve, 3000));
123+
124+
// 5. Find and click the confirm button
125+
const doneButtons = document.querySelectorAll('button');
126+
console.log('doneButtons', doneButtons);
127+
128+
const doneButton = Array.from(doneButtons).find((e) => e.textContent === '确定');
129+
console.log('doneButton', doneButton);
130+
131+
if (doneButton) {
132+
(doneButton as HTMLElement).click();
133+
}
134+
}
135+
73136
try {
74-
const { content, video, title, tags } = data.data as VideoData;
137+
const { content, video, title, tags, cover } = data.data as VideoData;
75138

76139
if (!video) {
77140
console.error('没有视频文件');
@@ -132,6 +195,10 @@ export async function VideoBaijiahao(data: SyncData) {
132195
}
133196
}
134197

198+
if (cover) {
199+
await uploadCover(cover);
200+
}
201+
135202
// 等待页面响应
136203
await new Promise((resolve) => setTimeout(resolve, 5000));
137204

src/sync/video/bilibili.ts

Lines changed: 103 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { type SyncData, type VideoData } from '../common';
1+
import { type FileData, type SyncData, type VideoData } from '../common';
22

33
export async function VideoBilibili(data: SyncData) {
44
function waitForElement(selector: string, timeout = 10000): Promise<Element> {
@@ -29,8 +29,76 @@ export async function VideoBilibili(data: SyncData) {
2929
});
3030
}
3131

32+
async function uploadCover(cover: FileData): Promise<void> {
33+
console.log('开始上传封面', cover);
34+
await waitForElement('div.cover-main-img > div.img');
35+
const coverUploadButton = document.querySelector('div.cover-main-img > div.img') as HTMLElement;
36+
if (!coverUploadButton) {
37+
console.log('未找到封面上传按钮');
38+
return;
39+
}
40+
41+
coverUploadButton.click();
42+
await new Promise((resolve) => setTimeout(resolve, 1000));
43+
44+
const tabContainer = document.querySelector('div.cover-select-header-tab');
45+
if (!tabContainer) {
46+
console.log('未找到封面选择的tab容器');
47+
return;
48+
}
49+
50+
const uploadTab = tabContainer.firstChild?.nextSibling as HTMLElement;
51+
if (!uploadTab) {
52+
console.log('未找到上传封面tab');
53+
return;
54+
}
55+
uploadTab.click();
56+
await new Promise((resolve) => setTimeout(resolve, 1000));
57+
58+
const fileInput = document.querySelector(
59+
"div.bcc-upload-wrapper > input[type='file'][accept='image/png, image/jpeg']",
60+
) as HTMLInputElement;
61+
62+
if (!fileInput) {
63+
console.log('未找到封面上传的文件输入框');
64+
return;
65+
}
66+
67+
const dataTransfer = new DataTransfer();
68+
if (!cover.type.includes('image/')) {
69+
console.log('封面文件类型不正确');
70+
return;
71+
}
72+
73+
const response = await fetch(cover.url);
74+
const blob = await response.blob();
75+
const coverFile = new File([blob], cover.name, { type: cover.type });
76+
dataTransfer.items.add(coverFile);
77+
78+
if (dataTransfer.files.length === 0) {
79+
return;
80+
}
81+
82+
fileInput.files = dataTransfer.files;
83+
fileInput.dispatchEvent(new Event('change', { bubbles: true }));
84+
fileInput.dispatchEvent(new Event('input', { bubbles: true }));
85+
86+
console.log('封面文件上传操作已触发');
87+
await new Promise((resolve) => setTimeout(resolve, 3000));
88+
89+
const doneButtons = document.querySelectorAll('div.cover-select-footer-pick button');
90+
const doneButton = Array.from(doneButtons).find((btn) => btn.textContent === ' 完成 ');
91+
92+
if (doneButton) {
93+
(doneButton as HTMLElement).click();
94+
console.log('封面上传完成');
95+
} else {
96+
console.log('未找到"完成"按钮');
97+
}
98+
}
99+
32100
async function uploadVideo(file: File): Promise<void> {
33-
const fileInput = (await waitForElement('input[type=file][multiple="multiple"]')) as HTMLInputElement;
101+
const fileInput = (await waitForElement('input[type="file"]')) as HTMLInputElement;
34102

35103
// 创建一个新的 File 对象,因为某些浏览器可能不允许直接设置 fileInput.files
36104
const dataTransfer = new DataTransfer();
@@ -66,12 +134,19 @@ export async function VideoBilibili(data: SyncData) {
66134
}
67135

68136
try {
69-
const { content, video, title, tags } = data.data as VideoData;
137+
const { content, video, title, tags, cover } = data.data as VideoData;
138+
70139
// 处理视频上传
71140
if (video) {
141+
await waitForElement('input[type="file"]');
142+
await new Promise((resolve) => setTimeout(resolve, 1000));
143+
72144
const response = await fetch(video.url);
73-
const blob = await response.blob();
74-
const videoFile = new File([blob], video.name, { type: video.type });
145+
const blob = await response.arrayBuffer();
146+
const extension = video.name.split('.').pop() || 'mp4';
147+
const videoFilename = `${title}.${extension}`;
148+
const videoFile = new File([blob], videoFilename, { type: video.type });
149+
75150
console.log(`视频文件: ${videoFile.name} ${videoFile.type} ${videoFile.size}`);
76151

77152
await uploadVideo(videoFile);
@@ -89,11 +164,9 @@ export async function VideoBilibili(data: SyncData) {
89164
return;
90165
}
91166

92-
await new Promise((resolve) => setTimeout(resolve, 5000));
93-
94167
// 处理标题输入
168+
const titleInput = (await waitForElement('input.input-val[type="text"][maxlength="80"]')) as HTMLInputElement;
95169
if (title) {
96-
const titleInput = (await waitForElement('input.input-val[type="text"][maxlength="80"]')) as HTMLInputElement;
97170
titleInput.focus();
98171
titleInput.value = title;
99172
titleInput.dispatchEvent(new Event('input', { bubbles: true }));
@@ -102,41 +175,32 @@ export async function VideoBilibili(data: SyncData) {
102175
}
103176

104177
// 等待简介编辑器出现并输入内容
105-
const editor = (await waitForElement('div[data-placeholder*="简介"]')) as HTMLDivElement;
106-
await new Promise((resolve) => setTimeout(resolve, 1000));
107-
108-
// 直接设置文本内容
109-
const contentToInsert = content || title || '';
110-
editor.textContent = contentToInsert;
111-
112-
// 触发 input 事件
113-
const inputEvent = new InputEvent('input', {
114-
bubbles: true,
115-
cancelable: true,
116-
inputType: 'insertText',
117-
data: contentToInsert,
118-
});
119-
editor.dispatchEvent(inputEvent);
178+
const editor = (await waitForElement('div.ql-editor[contenteditable="true"]')) as HTMLDivElement;
179+
if (editor) {
180+
editor.innerHTML = content || '';
181+
console.log('简介已输入:', content);
182+
}
120183

121-
console.log('简介已输入:', contentToInsert);
184+
await new Promise((resolve) => setTimeout(resolve, 3000));
122185

123186
// 处理标签
124-
await new Promise((resolve) => setTimeout(resolve, 5000));
125-
126187
// 清除已有标签
127188
const existingTags = document.querySelectorAll('div.tag-pre-wrp > div.label-item-v2-container');
128-
console.log('正在清除已有标签...');
129-
for (let i = 0; i < 20 && existingTags.length > 0; i++) {
130-
const tag = existingTags[0] as HTMLElement;
131-
tag.click();
132-
await new Promise((resolve) => setTimeout(resolve, 400));
189+
console.log(`发现 ${existingTags.length} 个已有标签,准备清除...`);
190+
for (let i = 0; i < existingTags.length; i++) {
191+
const tag = existingTags[i] as HTMLElement;
192+
const closeButton = tag.querySelector('.label-item-v2-close');
193+
if (closeButton) {
194+
(closeButton as HTMLElement).click();
195+
await new Promise((resolve) => setTimeout(resolve, 400));
196+
}
133197
}
134198

135199
if (!tags || tags.length === 0) {
136200
// 如果没有指定标签,选择热门标签
137201
console.log('未指定标签,选择热门标签...');
138202
const hotTags = document.querySelectorAll('.hot-tag-item');
139-
if (hotTags) {
203+
if (hotTags.length > 0) {
140204
for (let i = 0; i < 3 && i < hotTags.length; i++) {
141205
const tag = hotTags[i] as HTMLElement;
142206
tag.click();
@@ -148,7 +212,7 @@ export async function VideoBilibili(data: SyncData) {
148212
console.log('添加指定标签...');
149213
const tagInput = document.querySelector('input[placeholder="按回车键Enter创建标签"]') as HTMLInputElement;
150214
if (tagInput) {
151-
for (const tag of tags) {
215+
for (const tag of tags.slice(0, 10)) {
152216
tagInput.value = tag;
153217
const enterEvent = new KeyboardEvent('keydown', {
154218
bubbles: true,
@@ -164,7 +228,12 @@ export async function VideoBilibili(data: SyncData) {
164228
}
165229
}
166230

167-
// 等待标签处理完成
231+
// 上传封面
232+
if (cover) {
233+
await uploadCover(cover);
234+
}
235+
236+
// 等待标签和封面处理完成
168237
await new Promise((resolve) => setTimeout(resolve, 5000));
169238

170239
// 如果需要自动发布

src/sync/video/douyin.ts

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { SyncData, VideoData } from '../common';
1+
import type { FileData, SyncData, VideoData } from '../common';
22

33
export async function VideoDouyin(data: SyncData) {
44
function waitForElement(selector: string, timeout = 10000): Promise<Element> {
@@ -48,8 +48,56 @@ export async function VideoDouyin(data: SyncData) {
4848
console.log('视频上传事件已触发');
4949
}
5050

51+
async function uploadCover(cover: FileData): Promise<void> {
52+
console.log('尝试上传封面', cover);
53+
const coverUploadContainer = await waitForElement('div.content-upload-new');
54+
console.log('封面上传容器', coverUploadContainer);
55+
if (!coverUploadContainer) return;
56+
57+
const coverUploadButton = coverUploadContainer.firstChild?.firstChild?.firstChild as HTMLElement;
58+
console.log('封面上传按钮', coverUploadButton);
59+
if (!coverUploadButton) return;
60+
61+
coverUploadButton.click();
62+
await new Promise((resolve) => setTimeout(resolve, 1000));
63+
64+
const fileInput = (await waitForElement('input[type="file"].semi-upload-hidden-input')) as HTMLInputElement;
65+
console.log('封面文件输入框', fileInput);
66+
if (!fileInput) return;
67+
68+
if (!cover.type?.includes('image/')) {
69+
console.log('提供的封面文件不是图片类型', cover);
70+
return;
71+
}
72+
73+
const response = await fetch(cover.url);
74+
const arrayBuffer = await response.arrayBuffer();
75+
const imageFile = new File([arrayBuffer], cover.name, { type: cover.type });
76+
77+
const dataTransfer = new DataTransfer();
78+
dataTransfer.items.add(imageFile);
79+
fileInput.files = dataTransfer.files;
80+
81+
const changeEvent = new Event('change', { bubbles: true });
82+
fileInput.dispatchEvent(changeEvent);
83+
84+
const inputEvent = new Event('input', { bubbles: true });
85+
fileInput.dispatchEvent(inputEvent);
86+
87+
console.log('封面文件上传操作已触发');
88+
await new Promise((resolve) => setTimeout(resolve, 3000));
89+
90+
const doneButtons = document.querySelectorAll('button.semi-button.semi-button-primary.semi-button-light');
91+
console.log('完成按钮列表', doneButtons);
92+
const doneButton = Array.from(doneButtons).find((button) => button.textContent === '完成');
93+
console.log('完成按钮', doneButton);
94+
if (doneButton) {
95+
(doneButton as HTMLElement).click();
96+
}
97+
}
98+
5199
try {
52-
const { content, video, title, tags } = data.data as VideoData;
100+
const { content, video, title, tags, cover } = data.data as VideoData;
53101
// 处理视频上传
54102
if (video) {
55103
const response = await fetch(video.url);
@@ -78,8 +126,8 @@ export async function VideoDouyin(data: SyncData) {
78126
if (contentEditor) {
79127
// 处理标签
80128
if (tags && tags.length > 0) {
81-
const reversedTags = [...tags].reverse();
82-
for (const tag of reversedTags) {
129+
const tagsToSync = tags.slice(0, 5);
130+
for (const tag of tagsToSync) {
83131
console.log('添加标签:', tag);
84132
contentEditor.focus();
85133

@@ -120,6 +168,12 @@ export async function VideoDouyin(data: SyncData) {
120168
contentEditor.dispatchEvent(contentPasteEvent);
121169
}
122170

171+
// 处理封面上传
172+
if (cover) {
173+
await new Promise((resolve) => setTimeout(resolve, 2000));
174+
await uploadCover(cover);
175+
}
176+
123177
await new Promise((resolve) => setTimeout(resolve, 5000));
124178

125179
// 处理自动发布

0 commit comments

Comments
 (0)