返回 Social Auto Upload
sau_cli.py
根目录 / sau_cli.py
1 from __future__ import annotations
2
3 import argparse
4 import asyncio
5 import sys
6 from dataclasses import dataclass
7 from datetime import datetime
8 from pathlib import Path
9 from typing import Iterable, Sequence
10
11 from conf import BASE_DIR
12 from uploader.baijiahao_uploader.main import (
13 BaiJiaHaoVideo,
14 baijiahao_setup,
15 cookie_auth as baijiahao_cookie_auth,
16 )
17 from uploader.alipay_uploader.main import (
18 AlipayVideo,
19 alipay_setup,
20 cookie_auth as alipay_cookie_auth,
21 )
22 from uploader.bilibili_uploader.runtime import run_biliup_command
23 from uploader.douyin_uploader.main import (
24 DOUYIN_PUBLISH_STRATEGY_IMMEDIATE,
25 DOUYIN_PUBLISH_STRATEGY_SCHEDULED,
26 DouYinNote,
27 DouYinVideo,
28 cookie_auth as douyin_cookie_auth,
29 douyin_setup,
30 )
31 from uploader.ks_uploader.main import (
32 KUAISHOU_PUBLISH_STRATEGY_IMMEDIATE,
33 KUAISHOU_PUBLISH_STRATEGY_SCHEDULED,
34 KSNote,
35 KSVideo,
36 cookie_auth as kuaishou_cookie_auth,
37 ks_setup,
38 )
39 from uploader.tencent_uploader.main import (
40 TENCENT_PUBLISH_STRATEGY_IMMEDIATE,
41 TENCENT_PUBLISH_STRATEGY_SCHEDULED,
42 TencentVideo,
43 cookie_auth as tencent_cookie_auth,
44 tencent_setup,
45 )
46 from uploader.weibo_uploader.main import (
47 WeiBoVideo,
48 weibo_setup,
49 cookie_auth as weibo_cookie_auth,
50 )
51 from uploader.hupu_uploader.main import (
52 HuPuVideo,
53 hupu_setup,
54 cookie_auth as hupu_cookie_auth,
55 )
56 from uploader.xiaohongshu_uploader.main import (
57 XIAOHONGSHU_PUBLISH_STRATEGY_IMMEDIATE,
58 XIAOHONGSHU_PUBLISH_STRATEGY_SCHEDULED,
59 XiaoHongShuNote,
60 XiaoHongShuVideo,
61 cookie_auth as xiaohongshu_cookie_auth,
62 xiaohongshu_setup,
63 )
64 from uploader.youtube_uploader.main import (
65 YouTubeVideo,
66 cookie_auth as youtube_cookie_auth,
67 youtube_setup,
68 )
69
70 SCHEDULE_FORMAT = "%Y-%m-%d %H:%M"
71
72
73 @dataclass(slots=True)
74 class DouyinVideoUploadRequest:
75 account_name: str
76 video_file: Path
77 title: str
78 description: str
79 tags: list[str]
80 publish_date: datetime | int
81 thumbnail_file: Path | None = None
82 thumbnail_landscape_file: Path | None = None
83 thumbnail_portrait_file: Path | None = None
84 product_link: str = ""
85 product_title: str = ""
86 publish_strategy: str = DOUYIN_PUBLISH_STRATEGY_IMMEDIATE
87 debug: bool = True
88 headless: bool = True
89 declaration: str | None = None
90 collection_name: str | None = None
91
92
93 @dataclass(slots=True)
94 class DouyinNoteUploadRequest:
95 account_name: str
96 image_files: list[Path]
97 title: str
98 note: str
99 tags: list[str]
100 publish_date: datetime | int
101 publish_strategy: str = DOUYIN_PUBLISH_STRATEGY_IMMEDIATE
102 debug: bool = True
103 headless: bool = True
104 bgm: str = ""
105
106
107 @dataclass(slots=True)
108 class KuaishouVideoUploadRequest:
109 account_name: str
110 video_file: Path
111 title: str
112 description: str
113 tags: list[str]
114 publish_date: datetime | int
115 thumbnail_file: Path | None = None
116 publish_strategy: str = KUAISHOU_PUBLISH_STRATEGY_IMMEDIATE
117 debug: bool = True
118 headless: bool = True
119 collection_name: str | None = None
120
121
122 @dataclass(slots=True)
123 class KuaishouNoteUploadRequest:
124 account_name: str
125 image_files: list[Path]
126 title: str
127 note: str
128 tags: list[str]
129 publish_date: datetime | int
130 publish_strategy: str = KUAISHOU_PUBLISH_STRATEGY_IMMEDIATE
131 debug: bool = True
132 headless: bool = True
133
134
135 @dataclass(slots=True)
136 class XiaohongshuVideoUploadRequest:
137 account_name: str
138 video_file: Path
139 title: str
140 description: str
141 tags: list[str]
142 publish_date: datetime | int
143 thumbnail_file: Path | None = None
144 publish_strategy: str = XIAOHONGSHU_PUBLISH_STRATEGY_IMMEDIATE
145 debug: bool = True
146 headless: bool = True
147
148
149 @dataclass(slots=True)
150 class XiaohongshuNoteUploadRequest:
151 account_name: str
152 image_files: list[Path]
153 title: str
154 note: str
155 tags: list[str]
156 publish_date: datetime | int
157 publish_strategy: str = XIAOHONGSHU_PUBLISH_STRATEGY_IMMEDIATE
158 debug: bool = True
159 headless: bool = True
160
161
162 @dataclass(slots=True)
163 class BilibiliVideoUploadRequest:
164 account_name: str
165 video_file: Path
166 title: str
167 description: str
168 tid: int
169 tags: list[str]
170 publish_date: datetime | int
171 thumbnail_file: Path | None = None
172
173
174 @dataclass(slots=True)
175 class TencentVideoUploadRequest:
176 account_name: str
177 video_file: Path
178 title: str
179 description: str
180 tags: list[str]
181 publish_date: datetime | int
182 thumbnail_file: Path | None = None
183 thumbnail_landscape_file: Path | None = None
184 thumbnail_portrait_file: Path | None = None
185 short_title: str | None = None
186 category: str | None = None
187 is_draft: bool = False
188 publish_strategy: str = TENCENT_PUBLISH_STRATEGY_IMMEDIATE
189 debug: bool = True
190 headless: bool = True
191 collection_name: str | None = None
192
193
194 @dataclass(slots=True)
195 class BaijiahaoVideoUploadRequest:
196 account_name: str
197 video_file: Path
198 title: str
199 description: str
200 tags: list[str]
201 thumbnail_file: Path | None = None
202 collection_name: str | None = None
203 debug: bool = True
204 headless: bool = True
205
206
207 @dataclass(slots=True)
208 class AlipayVideoUploadRequest:
209 account_name: str
210 video_file: Path
211 title: str
212 description: str
213 tags: list[str]
214 thumbnail_file: Path | None = None
215 collection_name: str | None = None
216 debug: bool = True
217 headless: bool = True
218
219
220 @dataclass(slots=True)
221 class WeiboVideoUploadRequest:
222 account_name: str
223 video_file: Path
224 title: str
225 description: str
226 tags: list[str]
227 thumbnail_file: Path | None = None
228 collection_name: str | None = None
229 debug: bool = True
230 headless: bool = True
231
232
233 @dataclass(slots=True)
234 class HupuVideoUploadRequest:
235 account_name: str
236 video_file: Path
237 title: str
238 description: str
239 tags: list[str]
240 thumbnail_file: Path | None = None
241 debug: bool = True
242 headless: bool = True
243
244
245 @dataclass(slots=True)
246 class YouTubeVideoUploadRequest:
247 account_name: str
248 video_file: Path
249 title: str
250 description: str
251 tags: list[str]
252 thumbnail_file: Path | None = None
253 playlist: str | None = None
254 visibility: str = "public"
255 debug: bool = True
256 headless: bool = False
257
258
259 def has_interactive_terminal() -> bool:
260 return sys.stdin.isatty() and sys.stdout.isatty()
261
262
263 def resolve_runtime_home() -> Path:
264 return Path(BASE_DIR)
265
266
267 def resolve_account_file(platform: str, account_name: str) -> Path:
268 account_file = resolve_runtime_home() / "cookies" / f"{platform}_{account_name}.json"
269 account_file.parent.mkdir(exist_ok=True)
270 return account_file
271
272
273 def parse_tags(raw_tags: str | None) -> list[str]:
274 if not raw_tags:
275 return []
276
277 tags: list[str] = []
278 for item in raw_tags.split(","):
279 cleaned = item.strip().lstrip("#")
280 if cleaned:
281 tags.append(cleaned)
282 return tags
283
284
285 def parse_image_files(raw_files: Iterable[Path]) -> list[Path]:
286 return [Path(file) for file in raw_files]
287
288
289 def parse_schedule(raw_schedule: str | None) -> datetime | int:
290 if not raw_schedule:
291 return 0
292 return datetime.strptime(raw_schedule, SCHEDULE_FORMAT)
293
294
295 async def login_douyin_account(account_name: str, headless: bool = True) -> dict:
296 account_file = resolve_account_file("douyin", account_name)
297 return await douyin_setup(str(account_file), handle=True, return_detail=True, headless=headless)
298
299
300 async def check_douyin_account(account_name: str) -> bool:
301 account_file = resolve_account_file("douyin", account_name)
302 if not account_file.exists():
303 return False
304 return await douyin_cookie_auth(str(account_file))
305
306
307 async def login_kuaishou_account(account_name: str, headless: bool = True) -> dict:
308 account_file = resolve_account_file("kuaishou", account_name)
309 return await ks_setup(str(account_file), handle=True, return_detail=True, headless=headless)
310
311
312 async def check_kuaishou_account(account_name: str) -> bool:
313 account_file = resolve_account_file("kuaishou", account_name)
314 if not account_file.exists():
315 return False
316 return await kuaishou_cookie_auth(str(account_file))
317
318
319 async def login_xiaohongshu_account(account_name: str, headless: bool = True) -> dict:
320 account_file = resolve_account_file("xiaohongshu", account_name)
321 return await xiaohongshu_setup(str(account_file), handle=True, return_detail=True, headless=headless)
322
323
324 async def check_xiaohongshu_account(account_name: str) -> bool:
325 account_file = resolve_account_file("xiaohongshu", account_name)
326 if not account_file.exists():
327 return False
328 return await xiaohongshu_cookie_auth(str(account_file))
329
330
331 async def login_bilibili_account(account_name: str) -> dict:
332 account_file = resolve_account_file("bilibili", account_name)
333 if not has_interactive_terminal():
334 return {
335 "success": False,
336 "message": (
337 "Bilibili login requires a local interactive terminal. "
338 f"Please run `sau bilibili login --account {account_name}` yourself in a local terminal. "
339 "If the terminal QR code does not render completely, open `./qrcode.png` and scan that image."
340 ),
341 "account_file": str(account_file),
342 }
343
344 result = run_biliup_command(["-u", str(account_file), "login"], interactive=True)
345 success = result.returncode == 0
346 return {
347 "success": success,
348 "message": (result.stderr or result.stdout or "").strip() or "Bilibili login completed" if success else (result.stderr or result.stdout or "").strip() or "Bilibili login failed",
349 "account_file": str(account_file),
350 }
351
352
353 async def check_bilibili_account(account_name: str) -> bool:
354 account_file = resolve_account_file("bilibili", account_name)
355 if not account_file.exists():
356 return False
357 result = run_biliup_command(["-u", str(account_file), "renew"])
358 return result.returncode == 0
359
360
361 async def login_tencent_account(account_name: str, headless: bool = True) -> dict:
362 account_file = resolve_account_file("tencent", account_name)
363 return await tencent_setup(str(account_file), handle=True, return_detail=True, headless=headless)
364
365
366 async def check_tencent_account(account_name: str) -> bool:
367 account_file = resolve_account_file("tencent", account_name)
368 if not account_file.exists():
369 return False
370 return await tencent_cookie_auth(str(account_file))
371
372
373 async def login_youtube_account(account_name: str, headless: bool = False) -> dict:
374 account_file = resolve_account_file("youtube", account_name)
375 return await youtube_setup(str(account_file), handle=True, return_detail=True, headless=headless)
376
377
378 async def check_youtube_account(account_name: str) -> bool:
379 account_file = resolve_account_file("youtube", account_name)
380 if not account_file.exists():
381 return False
382 return await youtube_cookie_auth(str(account_file))
383
384
385 async def upload_youtube_video(request: YouTubeVideoUploadRequest) -> Path:
386 account_file = resolve_account_file("youtube", request.account_name)
387 is_ready = await youtube_setup(str(account_file), handle=False)
388 if not is_ready:
389 raise RuntimeError(
390 f"YouTube cookie is missing or expired: {account_file}. Run `sau youtube login --account {request.account_name}` first."
391 )
392
393 app = YouTubeVideo(
394 request.title,
395 str(request.video_file),
396 request.tags,
397 str(account_file),
398 description=request.description,
399 thumbnail_path=str(request.thumbnail_file) if request.thumbnail_file else None,
400 playlist=request.playlist,
401 visibility=request.visibility,
402 debug=request.debug,
403 headless=request.headless,
404 )
405 await app.main()
406 return account_file
407
408
409 async def upload_video(request: DouyinVideoUploadRequest) -> Path:
410 account_file = resolve_account_file("douyin", request.account_name)
411 is_ready = await douyin_setup(str(account_file), handle=False)
412 if not is_ready:
413 raise RuntimeError(
414 f"Douyin cookie is missing or expired: {account_file}. Run `sau douyin login --account {request.account_name}` first."
415 )
416
417 app = DouYinVideo(
418 request.title,
419 str(request.video_file),
420 request.tags,
421 request.publish_date,
422 str(account_file),
423 desc=request.description,
424 thumbnail_landscape_path=(
425 str(request.thumbnail_landscape_file) if request.thumbnail_landscape_file else None
426 ),
427 thumbnail_portrait_path=str(
428 request.thumbnail_portrait_file or request.thumbnail_file
429 ) if request.thumbnail_portrait_file or request.thumbnail_file else None,
430 productLink=request.product_link,
431 productTitle=request.product_title,
432 declaration=request.declaration,
433 publish_strategy=request.publish_strategy,
434 debug=request.debug,
435 headless=request.headless,
436 collection_name=request.collection_name,
437 )
438 await app.douyin_upload_video()
439 return account_file
440
441
442 async def upload_note(request: DouyinNoteUploadRequest) -> Path:
443 account_file = resolve_account_file("douyin", request.account_name)
444 is_ready = await douyin_setup(str(account_file), handle=False)
445 if not is_ready:
446 raise RuntimeError(
447 f"Douyin cookie is missing or expired: {account_file}. Run `sau douyin login --account {request.account_name}` first."
448 )
449
450 app = DouYinNote(
451 image_paths=[str(path) for path in request.image_files],
452 title=request.title,
453 note=request.note,
454 tags=request.tags,
455 publish_date=request.publish_date,
456 account_file=str(account_file),
457 publish_strategy=request.publish_strategy,
458 debug=request.debug,
459 headless=request.headless,
460 bgm=request.bgm,
461 )
462 await app.douyin_upload_note()
463 return account_file
464
465
466 async def upload_kuaishou_video(request: KuaishouVideoUploadRequest) -> Path:
467 account_file = resolve_account_file("kuaishou", request.account_name)
468 is_ready = await ks_setup(str(account_file), handle=False)
469 if not is_ready:
470 raise RuntimeError(
471 f"Kuaishou cookie is missing or expired: {account_file}. Run `sau kuaishou login --account {request.account_name}` first."
472 )
473
474 app = KSVideo(
475 title=request.title,
476 file_path=str(request.video_file),
477 desc=request.description,
478 tags=request.tags,
479 publish_date=request.publish_date,
480 account_file=str(account_file),
481 thumbnail_path=str(request.thumbnail_file) if request.thumbnail_file else None,
482 publish_strategy=request.publish_strategy,
483 debug=request.debug,
484 headless=request.headless,
485 collection_name=request.collection_name,
486 )
487 await app.main()
488 return account_file
489
490
491 async def upload_kuaishou_note(request: KuaishouNoteUploadRequest) -> Path:
492 account_file = resolve_account_file("kuaishou", request.account_name)
493 is_ready = await ks_setup(str(account_file), handle=False)
494 if not is_ready:
495 raise RuntimeError(
496 f"Kuaishou cookie is missing or expired: {account_file}. Run `sau kuaishou login --account {request.account_name}` first."
497 )
498
499 app = KSNote(
500 image_paths=[str(path) for path in request.image_files],
501 title=request.title,
502 note=request.note,
503 tags=request.tags,
504 publish_date=request.publish_date,
505 account_file=str(account_file),
506 publish_strategy=request.publish_strategy,
507 debug=request.debug,
508 headless=request.headless,
509 )
510 await app.main()
511 return account_file
512
513
514 async def upload_xiaohongshu_video(request: XiaohongshuVideoUploadRequest) -> Path:
515 account_file = resolve_account_file("xiaohongshu", request.account_name)
516 is_ready = await xiaohongshu_setup(str(account_file), handle=False)
517 if not is_ready:
518 raise RuntimeError(
519 f"Xiaohongshu cookie is missing or expired: {account_file}. Run `sau xiaohongshu login --account {request.account_name}` first."
520 )
521
522 app = XiaoHongShuVideo(
523 title=request.title,
524 file_path=str(request.video_file),
525 desc=request.description,
526 tags=request.tags,
527 publish_date=request.publish_date,
528 account_file=str(account_file),
529 thumbnail_path=str(request.thumbnail_file) if request.thumbnail_file else None,
530 publish_strategy=request.publish_strategy,
531 debug=request.debug,
532 headless=request.headless,
533 )
534 await app.main()
535 return account_file
536
537
538 async def upload_xiaohongshu_note(request: XiaohongshuNoteUploadRequest) -> Path:
539 account_file = resolve_account_file("xiaohongshu", request.account_name)
540 is_ready = await xiaohongshu_setup(str(account_file), handle=False)
541 if not is_ready:
542 raise RuntimeError(
543 f"Xiaohongshu cookie is missing or expired: {account_file}. Run `sau xiaohongshu login --account {request.account_name}` first."
544 )
545
546 app = XiaoHongShuNote(
547 image_paths=[str(path) for path in request.image_files],
548 title=request.title,
549 desc=request.note,
550 note=request.note,
551 tags=request.tags,
552 publish_date=request.publish_date,
553 account_file=str(account_file),
554 publish_strategy=request.publish_strategy,
555 debug=request.debug,
556 headless=request.headless,
557 )
558 await app.main()
559 return account_file
560
561
562 async def upload_bilibili_video(request: BilibiliVideoUploadRequest) -> Path:
563 account_file = resolve_account_file("bilibili", request.account_name)
564 if not account_file.exists():
565 raise RuntimeError(
566 f"Bilibili account file is missing: {account_file}. Run `sau bilibili login --account {request.account_name}` first."
567 )
568
569 arguments = [
570 "-u",
571 str(account_file),
572 "upload",
573 str(request.video_file),
574 "--title",
575 request.title,
576 "--desc",
577 request.description,
578 "--tid",
579 str(request.tid),
580 ]
581 if request.tags:
582 arguments.extend(["--tag", ",".join(request.tags)])
583 if request.thumbnail_file:
584 arguments.extend(["--cover", str(request.thumbnail_file)])
585 if isinstance(request.publish_date, datetime):
586 arguments.extend(["--dtime", str(int(request.publish_date.timestamp()))])
587
588 result = run_biliup_command(arguments)
589 if result.returncode != 0:
590 raise RuntimeError((result.stderr or result.stdout or "").strip() or "Bilibili upload failed")
591 return account_file
592
593
594 async def upload_tencent_video(request: TencentVideoUploadRequest) -> Path:
595 account_file = resolve_account_file("tencent", request.account_name)
596 is_ready = await tencent_setup(str(account_file), handle=False)
597 if not is_ready:
598 raise RuntimeError(
599 f"Tencent/WeChat Channels cookie is missing or expired: {account_file}. "
600 f"Run `sau tencent login --account {request.account_name}` first."
601 )
602
603 app = TencentVideo(
604 title=request.title,
605 file_path=str(request.video_file),
606 tags=request.tags,
607 publish_date=request.publish_date,
608 account_file=str(account_file),
609 category=request.category,
610 is_draft=request.is_draft,
611 desc=request.description,
612 thumbnail_path=str(request.thumbnail_file) if request.thumbnail_file else None,
613 thumbnail_landscape_path=(
614 str(request.thumbnail_landscape_file) if request.thumbnail_landscape_file else None
615 ),
616 thumbnail_portrait_path=(
617 str(request.thumbnail_portrait_file) if request.thumbnail_portrait_file else None
618 ),
619 short_title=request.short_title,
620 publish_strategy=request.publish_strategy,
621 debug=request.debug,
622 headless=request.headless,
623 collection_name=request.collection_name,
624 )
625 await app.tencent_upload_video()
626 return account_file
627
628
629 async def login_baijiahao_account(account_name: str, headless: bool = True, qrcode_callback=None) -> dict:
630 account_file = resolve_account_file("baijiahao", account_name)
631 return await baijiahao_setup(str(account_file), handle=True, return_detail=True, headless=headless, qrcode_callback=qrcode_callback)
632
633
634 async def check_baijiahao_account(account_name: str) -> bool:
635 account_file = resolve_account_file("baijiahao", account_name)
636 if not account_file.exists():
637 return False
638 return await baijiahao_cookie_auth(str(account_file))
639
640
641 async def upload_baijiahao_video(request: BaijiahaoVideoUploadRequest) -> Path:
642 account_file = resolve_account_file("baijiahao", request.account_name)
643 is_ready = await baijiahao_setup(str(account_file), handle=False)
644 if not is_ready:
645 raise RuntimeError(
646 f"Baijiahao cookie is missing or expired: {account_file}. Run `sau baijiahao login --account {request.account_name}` first."
647 )
648
649 app = BaiJiaHaoVideo(
650 title=request.title,
651 file_path=str(request.video_file),
652 tags=request.tags,
653 account_file=str(account_file),
654 desc=request.description,
655 thumbnail_path=str(request.thumbnail_file) if request.thumbnail_file else None,
656 collection_name=request.collection_name,
657 debug=request.debug,
658 headless=request.headless,
659 )
660 await app.main()
661 return account_file
662
663
664 async def login_alipay_account(account_name: str, headless: bool = True, qrcode_callback=None) -> dict:
665 account_file = resolve_account_file("alipay", account_name)
666 return await alipay_setup(str(account_file), handle=True, return_detail=True, headless=headless, qrcode_callback=qrcode_callback)
667
668
669 async def check_alipay_account(account_name: str) -> bool:
670 account_file = resolve_account_file("alipay", account_name)
671 if not account_file.exists():
672 return False
673 return await alipay_cookie_auth(str(account_file))
674
675
676 async def upload_alipay_video(request: AlipayVideoUploadRequest) -> Path:
677 account_file = resolve_account_file("alipay", request.account_name)
678 is_ready = await alipay_setup(str(account_file), handle=False)
679 if not is_ready:
680 raise RuntimeError(
681 f"Alipay cookie is missing or expired: {account_file}. Run `sau alipay login --account {request.account_name}` first."
682 )
683
684 app = AlipayVideo(
685 title=request.title,
686 file_path=str(request.video_file),
687 tags=request.tags,
688 account_file=str(account_file),
689 desc=request.description,
690 thumbnail_path=str(request.thumbnail_file) if request.thumbnail_file else None,
691 collection_name=request.collection_name,
692 debug=request.debug,
693 headless=request.headless,
694 )
695 await app.main()
696 return account_file
697
698
699 async def login_weibo_account(account_name: str, headless: bool = True, qrcode_callback=None) -> dict:
700 account_file = resolve_account_file("weibo", account_name)
701 return await weibo_setup(str(account_file), handle=True, return_detail=True, headless=headless, qrcode_callback=qrcode_callback)
702
703
704 async def check_weibo_account(account_name: str) -> bool:
705 account_file = resolve_account_file("weibo", account_name)
706 if not account_file.exists():
707 return False
708 return await weibo_cookie_auth(str(account_file))
709
710
711 async def upload_weibo_video(request: WeiboVideoUploadRequest) -> Path:
712 account_file = resolve_account_file("weibo", request.account_name)
713 is_ready = await weibo_setup(str(account_file), handle=False)
714 if not is_ready:
715 raise RuntimeError(
716 f"Weibo cookie is missing or expired: {account_file}. Run `sau weibo login --account {request.account_name}` first."
717 )
718
719 app = WeiBoVideo(
720 title=request.title,
721 file_path=str(request.video_file),
722 tags=request.tags,
723 account_file=str(account_file),
724 desc=request.description,
725 thumbnail_path=str(request.thumbnail_file) if request.thumbnail_file else None,
726 collection_name=request.collection_name,
727 debug=request.debug,
728 headless=request.headless,
729 )
730 await app.main()
731 return account_file
732
733
734 async def login_hupu_account(account_name: str, headless: bool = False, qrcode_callback=None) -> dict:
735 account_file = resolve_account_file("hupu", account_name)
736 return await hupu_setup(str(account_file), handle=True, return_detail=True, headless=headless, qrcode_callback=qrcode_callback)
737
738
739 async def check_hupu_account(account_name: str) -> bool:
740 account_file = resolve_account_file("hupu", account_name)
741 if not account_file.exists():
742 return False
743 return await hupu_cookie_auth(str(account_file))
744
745
746 async def upload_hupu_video(request: HupuVideoUploadRequest) -> Path:
747 account_file = resolve_account_file("hupu", request.account_name)
748 is_ready = await hupu_setup(str(account_file), handle=False)
749 if not is_ready:
750 raise RuntimeError(
751 f"Hupu cookie is missing or expired: {account_file}. Run `sau hupu login --account {request.account_name}` first."
752 )
753
754 app = HuPuVideo(
755 title=request.title,
756 file_path=str(request.video_file),
757 tags=request.tags,
758 account_file=str(account_file),
759 desc=request.description,
760 thumbnail_path=str(request.thumbnail_file) if request.thumbnail_file else None,
761 debug=request.debug,
762 headless=request.headless,
763 )
764 await app.main()
765 return account_file
766
767
768 def existing_file_path(value: str) -> Path:
769 path = Path(value)
770 if not path.is_file():
771 raise argparse.ArgumentTypeError(f"File not found: {value}")
772 return path
773
774
775 def schedule_value(value: str):
776 try:
777 return parse_schedule(value)
778 except ValueError as exc:
779 raise argparse.ArgumentTypeError(
780 f"Invalid schedule '{value}'. Expected format: {SCHEDULE_FORMAT}"
781 ) from exc
782
783
784 def add_runtime_flags(parser: argparse.ArgumentParser) -> None:
785 parser.add_argument("--debug", action="store_true", help="Enable debug mode")
786 headless_group = parser.add_mutually_exclusive_group()
787 headless_group.add_argument("--headed", dest="headless", action="store_false", help="Run with browser UI")
788 headless_group.add_argument("--headless", dest="headless", action="store_true", help="Run in headless mode")
789 parser.set_defaults(headless=True)
790
791
792 def build_parser() -> argparse.ArgumentParser:
793 schedule_help = SCHEDULE_FORMAT.replace("%", "%%")
794 parser = argparse.ArgumentParser(
795 prog="sau",
796 description="CLI for social-auto-upload.",
797 )
798 platform_parsers = parser.add_subparsers(dest="platform", required=True)
799
800 douyin_parser = platform_parsers.add_parser("douyin", help="Douyin operations")
801 douyin_actions = douyin_parser.add_subparsers(dest="action", required=True)
802
803 for action_name in ("login", "check"):
804 action_parser = douyin_actions.add_parser(action_name, help=f"Douyin {action_name}")
805 action_parser.add_argument("--account", required=True, help="Douyin user-defined account_name")
806 if action_name == "login":
807 add_runtime_flags(action_parser)
808
809 upload_video_parser = douyin_actions.add_parser("upload-video", help="Upload one video to Douyin")
810 upload_video_parser.add_argument("--account", required=True, help="Douyin user-defined account_name")
811 upload_video_parser.add_argument("--file", required=True, type=existing_file_path, help="Video file path")
812 upload_video_parser.add_argument("--title", required=True, help="Video title")
813 upload_video_parser.add_argument("--desc", default="", help="Optional video description")
814 upload_video_parser.add_argument("--tags", default="", help="Comma-separated tags, such as tag1,tag2")
815 upload_video_parser.add_argument("--schedule", type=schedule_value, help=f"Schedule time in {schedule_help}")
816 upload_video_parser.add_argument("--thumbnail", type=existing_file_path, help="Optional 3:4 portrait thumbnail path")
817 upload_video_parser.add_argument("--thumbnail-landscape", type=existing_file_path, help="Optional 4:3 landscape thumbnail path")
818 upload_video_parser.add_argument("--thumbnail-portrait", type=existing_file_path, help="Optional 3:4 portrait thumbnail path")
819 upload_video_parser.add_argument("--product-link", default="", help="Optional product link")
820 upload_video_parser.add_argument("--product-title", default="", help="Optional product title")
821 upload_video_parser.add_argument(
822 "--declaration",
823 help="Exact Douyin self-declaration option text; omitted means do not set one",
824 )
825 upload_video_parser.add_argument("--collection", default=None, help="Optional collection name to add the work into (must already exist)")
826 add_runtime_flags(upload_video_parser)
827
828 upload_note_parser = douyin_actions.add_parser("upload-note", help="Upload one note to Douyin")
829 upload_note_parser.add_argument("--account", required=True, help="Douyin user-defined account_name")
830 upload_note_parser.add_argument("--images", required=True, nargs="+", type=existing_file_path, help="Image file paths")
831 upload_note_parser.add_argument("--title", required=True, help="Note title")
832 upload_note_parser.add_argument("--note", default="", help="Optional note content")
833 upload_note_parser.add_argument("--notef", default="", help="Read note content from file (txt/md)")
834 upload_note_parser.add_argument("--tags", default="", help="Comma-separated tags, such as tag1,tag2")
835 upload_note_parser.add_argument("--bgm", default="", help="BGM music name to search and select")
836 upload_note_parser.add_argument("--schedule", type=schedule_value, help=f"Schedule time in {schedule_help}")
837 add_runtime_flags(upload_note_parser)
838
839 kuaishou_parser = platform_parsers.add_parser("kuaishou", help="Kuaishou operations")
840 kuaishou_actions = kuaishou_parser.add_subparsers(dest="action", required=True)
841
842 for action_name in ("login", "check"):
843 action_parser = kuaishou_actions.add_parser(action_name, help=f"Kuaishou {action_name}")
844 action_parser.add_argument("--account", required=True, help="Kuaishou user-defined account_name")
845 if action_name == "login":
846 add_runtime_flags(action_parser)
847
848 kuaishou_upload_video_parser = kuaishou_actions.add_parser("upload-video", help="Upload one video to Kuaishou")
849 kuaishou_upload_video_parser.add_argument("--account", required=True, help="Kuaishou user-defined account_name")
850 kuaishou_upload_video_parser.add_argument("--file", required=True, type=existing_file_path, help="Video file path")
851 kuaishou_upload_video_parser.add_argument("--title", required=True, help="Video title")
852 kuaishou_upload_video_parser.add_argument("--desc", default="", help="Optional video description")
853 kuaishou_upload_video_parser.add_argument("--tags", default="", help="Comma-separated tags, such as tag1,tag2")
854 kuaishou_upload_video_parser.add_argument("--schedule", type=schedule_value, help=f"Schedule time in {schedule_help}")
855 kuaishou_upload_video_parser.add_argument("--thumbnail", type=existing_file_path, help="Optional thumbnail path")
856 kuaishou_upload_video_parser.add_argument("--collection", default=None, help="Optional collection name to add the work into (must already exist)")
857 add_runtime_flags(kuaishou_upload_video_parser)
858
859 kuaishou_upload_note_parser = kuaishou_actions.add_parser("upload-note", help="Upload one note to Kuaishou")
860 kuaishou_upload_note_parser.add_argument("--account", required=True, help="Kuaishou user-defined account_name")
861 kuaishou_upload_note_parser.add_argument("--images", required=True, nargs="+", type=existing_file_path, help="Image file paths")
862 kuaishou_upload_note_parser.add_argument("--title", required=True, help="Note title")
863 kuaishou_upload_note_parser.add_argument("--note", default="", help="Optional note content")
864 kuaishou_upload_note_parser.add_argument("--tags", default="", help="Comma-separated tags, such as tag1,tag2")
865 kuaishou_upload_note_parser.add_argument("--schedule", type=schedule_value, help=f"Schedule time in {schedule_help}")
866 add_runtime_flags(kuaishou_upload_note_parser)
867
868 xiaohongshu_parser = platform_parsers.add_parser("xiaohongshu", help="Xiaohongshu operations")
869 xiaohongshu_actions = xiaohongshu_parser.add_subparsers(dest="action", required=True)
870
871 for action_name in ("login", "check"):
872 action_parser = xiaohongshu_actions.add_parser(action_name, help=f"Xiaohongshu {action_name}")
873 action_parser.add_argument("--account", required=True, help="Xiaohongshu user-defined account_name")
874 if action_name == "login":
875 add_runtime_flags(action_parser)
876
877 xiaohongshu_upload_video_parser = xiaohongshu_actions.add_parser("upload-video", help="Upload one video to Xiaohongshu")
878 xiaohongshu_upload_video_parser.add_argument("--account", required=True, help="Xiaohongshu user-defined account_name")
879 xiaohongshu_upload_video_parser.add_argument("--file", required=True, type=existing_file_path, help="Video file path")
880 xiaohongshu_upload_video_parser.add_argument("--title", required=True, help="Video title")
881 xiaohongshu_upload_video_parser.add_argument("--desc", default="", help="Optional video description")
882 xiaohongshu_upload_video_parser.add_argument("--tags", default="", help="Comma-separated tags, such as tag1,tag2")
883 xiaohongshu_upload_video_parser.add_argument("--schedule", type=schedule_value, help=f"Schedule time in {schedule_help}")
884 xiaohongshu_upload_video_parser.add_argument("--thumbnail", type=existing_file_path, help="Optional thumbnail path")
885 add_runtime_flags(xiaohongshu_upload_video_parser)
886
887 xiaohongshu_upload_note_parser = xiaohongshu_actions.add_parser("upload-note", help="Upload one note to Xiaohongshu")
888 xiaohongshu_upload_note_parser.add_argument("--account", required=True, help="Xiaohongshu user-defined account_name")
889 xiaohongshu_upload_note_parser.add_argument("--images", required=True, nargs="+", type=existing_file_path, help="Image file paths")
890 xiaohongshu_upload_note_parser.add_argument("--title", required=True, help="Note title")
891 xiaohongshu_upload_note_parser.add_argument("--note", default="", help="Optional note content")
892 xiaohongshu_upload_note_parser.add_argument("--tags", default="", help="Comma-separated tags, such as tag1,tag2")
893 xiaohongshu_upload_note_parser.add_argument("--schedule", type=schedule_value, help=f"Schedule time in {schedule_help}")
894 add_runtime_flags(xiaohongshu_upload_note_parser)
895
896 bilibili_parser = platform_parsers.add_parser("bilibili", help="Bilibili operations")
897 bilibili_actions = bilibili_parser.add_subparsers(dest="action", required=True)
898
899 for action_name in ("login", "check"):
900 action_parser = bilibili_actions.add_parser(action_name, help=f"Bilibili {action_name}")
901 action_parser.add_argument("--account", required=True, help="Bilibili user-defined account_name")
902
903 bilibili_upload_video_parser = bilibili_actions.add_parser("upload-video", help="Upload one video to Bilibili")
904 bilibili_upload_video_parser.add_argument("--account", required=True, help="Bilibili user-defined account_name")
905 bilibili_upload_video_parser.add_argument("--file", required=True, type=existing_file_path, help="Video file path")
906 bilibili_upload_video_parser.add_argument("--title", required=True, help="Video title")
907 bilibili_upload_video_parser.add_argument("--desc", required=True, help="Video description")
908 bilibili_upload_video_parser.add_argument("--tid", required=True, type=int, help="Bilibili category id")
909 bilibili_upload_video_parser.add_argument("--tags", default="", help="Comma-separated tags, such as tag1,tag2")
910 bilibili_upload_video_parser.add_argument("--thumbnail", type=existing_file_path, help="Optional Bilibili cover image path")
911 bilibili_upload_video_parser.add_argument("--schedule", type=schedule_value, help=f"Schedule time in {schedule_help}")
912
913 tencent_parser = platform_parsers.add_parser("tencent", help="Tencent/WeChat Channels operations")
914 tencent_actions = tencent_parser.add_subparsers(dest="action", required=True)
915
916 for action_name in ("login", "check"):
917 action_parser = tencent_actions.add_parser(action_name, help=f"Tencent/WeChat Channels {action_name}")
918 action_parser.add_argument("--account", required=True, help="Tencent user-defined account_name")
919 if action_name == "login":
920 add_runtime_flags(action_parser)
921
922 tencent_upload_video_parser = tencent_actions.add_parser("upload-video", help="Upload one video to WeChat Channels")
923 tencent_upload_video_parser.add_argument("--account", required=True, help="Tencent user-defined account_name")
924 tencent_upload_video_parser.add_argument("--file", required=True, type=existing_file_path, help="Video file path")
925 tencent_upload_video_parser.add_argument("--title", required=True, help="Video title")
926 tencent_upload_video_parser.add_argument("--desc", default="", help="Optional video description")
927 tencent_upload_video_parser.add_argument("--tags", default="", help="Comma-separated tags, such as tag1,tag2")
928 tencent_upload_video_parser.add_argument("--schedule", type=schedule_value, help=f"Schedule time in {schedule_help}")
929 tencent_upload_video_parser.add_argument("--thumbnail", type=existing_file_path, help="Optional 3:4 portrait thumbnail path")
930 tencent_upload_video_parser.add_argument("--thumbnail-landscape", type=existing_file_path, help="Optional 4:3 landscape thumbnail path")
931 tencent_upload_video_parser.add_argument("--thumbnail-portrait", type=existing_file_path, help="Optional 3:4 portrait thumbnail path")
932 tencent_upload_video_parser.add_argument("--short-title", help="Optional WeChat Channels short title")
933 tencent_upload_video_parser.add_argument("--category", help="Optional original content category")
934 tencent_upload_video_parser.add_argument("--draft", action="store_true", help="Save as draft instead of publishing")
935 tencent_upload_video_parser.add_argument("--collection", default=None, help="Optional collection name to add the work into (must already exist)")
936 add_runtime_flags(tencent_upload_video_parser)
937
938 alipay_parser = platform_parsers.add_parser("alipay", help="Alipay life account operations")
939 alipay_actions = alipay_parser.add_subparsers(dest="action", required=True)
940
941 for action_name in ("login", "check"):
942 action_parser = alipay_actions.add_parser(action_name, help=f"Alipay {action_name}")
943 action_parser.add_argument("--account", required=True, help="Alipay user-defined account_name")
944 if action_name == "login":
945 add_runtime_flags(action_parser)
946
947 alipay_upload_video_parser = alipay_actions.add_parser("upload-video", help="Upload one video to Alipay")
948 alipay_upload_video_parser.add_argument("--account", required=True, help="Alipay user-defined account_name")
949 alipay_upload_video_parser.add_argument("--file", required=True, type=existing_file_path, help="Video file path")
950 alipay_upload_video_parser.add_argument("--title", required=True, help="Video title")
951 alipay_upload_video_parser.add_argument("--desc", default="", help="Optional video description")
952 alipay_upload_video_parser.add_argument("--tags", default="", help="Comma-separated tags, such as tag1,tag2")
953 alipay_upload_video_parser.add_argument("--thumbnail", type=existing_file_path, help="Optional cover image path")
954 alipay_upload_video_parser.add_argument("--collection", default=None, help="Optional collection name to add the work into (must already exist)")
955 add_runtime_flags(alipay_upload_video_parser)
956
957 # ── Weibo ──
958 weibo_parser = platform_parsers.add_parser("weibo", help="Sina Weibo operations")
959 weibo_actions = weibo_parser.add_subparsers(dest="action", required=True)
960
961 for action_name in ("login", "check"):
962 action_parser = weibo_actions.add_parser(action_name, help=f"Weibo {action_name}")
963 action_parser.add_argument("--account", required=True, help="Weibo user-defined account_name")
964 if action_name == "login":
965 add_runtime_flags(action_parser)
966
967 weibo_upload_video_parser = weibo_actions.add_parser("upload-video", help="Upload one video to Weibo")
968 weibo_upload_video_parser.add_argument("--account", required=True, help="Weibo user-defined account_name")
969 weibo_upload_video_parser.add_argument("--file", required=True, type=existing_file_path, help="Video file path")
970 weibo_upload_video_parser.add_argument("--title", required=True, help="Video title (max 30 chars)")
971 weibo_upload_video_parser.add_argument("--desc", default="", help="Optional video description")
972 weibo_upload_video_parser.add_argument("--tags", default="", help="Comma-separated tags, such as tag1,tag2")
973 weibo_upload_video_parser.add_argument("--thumbnail", type=existing_file_path, help="Optional cover image path (<5MB)")
974 weibo_upload_video_parser.add_argument("--collection", default=None, help="Optional collection name")
975 add_runtime_flags(weibo_upload_video_parser)
976
977 # ── Hupu ──
978 hupu_parser = platform_parsers.add_parser("hupu", help="Hupu (虎扑) operations")
979 hupu_actions = hupu_parser.add_subparsers(dest="action", required=True)
980
981 for action_name in ("login", "check"):
982 action_parser = hupu_actions.add_parser(action_name, help=f"Hupu {action_name}")
983 action_parser.add_argument("--account", required=True, help="Hupu user-defined account_name")
984 if action_name == "login":
985 add_runtime_flags(action_parser)
986
987 hupu_upload_video_parser = hupu_actions.add_parser("upload-video", help="Upload one video to Hupu")
988 hupu_upload_video_parser.add_argument("--account", required=True, help="Hupu user-defined account_name")
989 hupu_upload_video_parser.add_argument("--file", required=True, type=existing_file_path, help="Video file path")
990 hupu_upload_video_parser.add_argument("--title", required=True, help="Video title (4-40 chars)")
991 hupu_upload_video_parser.add_argument("--desc", default="", help="Optional video description")
992 hupu_upload_video_parser.add_argument("--tags", default="", help="Comma-separated tags, such as tag1,tag2")
993 hupu_upload_video_parser.add_argument("--thumbnail", type=existing_file_path, help="Optional cover image path")
994 add_runtime_flags(hupu_upload_video_parser)
995
996 youtube_parser = platform_parsers.add_parser("youtube", help="YouTube operations")
997 youtube_actions = youtube_parser.add_subparsers(dest="action", required=True)
998
999 for action_name in ("login", "check"):
1000 action_parser = youtube_actions.add_parser(action_name, help=f"YouTube {action_name}")
1001 action_parser.add_argument("--account", required=True, help="YouTube user-defined account_name")
1002 if action_name == "login":
1003 add_runtime_flags(action_parser)
1004
1005 youtube_upload_video_parser = youtube_actions.add_parser("upload-video", help="Upload one video to YouTube")
1006 youtube_upload_video_parser.add_argument("--account", required=True, help="YouTube user-defined account_name")
1007 youtube_upload_video_parser.add_argument("--file", required=True, type=existing_file_path, help="Video file path")
1008 youtube_upload_video_parser.add_argument("--title", required=True, help="Video title (<=100 chars)")
1009 youtube_upload_video_parser.add_argument("--desc", default="", help="Optional video description")
1010 youtube_upload_video_parser.add_argument("--tags", default="", help="Comma-separated tags, such as tag1,tag2")
1011 youtube_upload_video_parser.add_argument("--thumbnail", type=existing_file_path, help="Optional thumbnail image path")
1012 youtube_upload_video_parser.add_argument("--playlist", help="Optional playlist name to add the video to (for series)")
1013 youtube_upload_video_parser.add_argument(
1014 "--visibility", default="public", choices=["public", "unlisted", "private"], help="Video visibility")
1015 add_runtime_flags(youtube_upload_video_parser)
1016
1017 baijiahao_parser = platform_parsers.add_parser("baijiahao", help="Baidu Baijiahao operations")
1018 baijiahao_actions = baijiahao_parser.add_subparsers(dest="action", required=True)
1019
1020 for action_name in ("login", "check"):
1021 action_parser = baijiahao_actions.add_parser(action_name, help=f"Baijiahao {action_name}")
1022 action_parser.add_argument("--account", required=True, help="Baijiahao user-defined account_name")
1023 if action_name == "login":
1024 add_runtime_flags(action_parser)
1025
1026 baijiahao_upload_video_parser = baijiahao_actions.add_parser("upload-video", help="Upload one video to Baijiahao")
1027 baijiahao_upload_video_parser.add_argument("--account", required=True, help="Baijiahao user-defined account_name")
1028 baijiahao_upload_video_parser.add_argument("--file", required=True, type=existing_file_path, help="Video file path")
1029 baijiahao_upload_video_parser.add_argument("--title", required=True, help="Video title")
1030 baijiahao_upload_video_parser.add_argument("--desc", default="", help="Optional video description")
1031 baijiahao_upload_video_parser.add_argument("--tags", default="", help="Comma-separated tags, such as tag1,tag2")
1032 baijiahao_upload_video_parser.add_argument("--thumbnail", type=existing_file_path, help="Optional cover image path")
1033 baijiahao_upload_video_parser.add_argument("--collection", default=None, help="Optional collection name")
1034 add_runtime_flags(baijiahao_upload_video_parser)
1035
1036 return parser
1037
1038
1039 async def dispatch(args: argparse.Namespace) -> int:
1040 if args.platform == "douyin":
1041 if args.action == "login":
1042 result = await login_douyin_account(args.account, headless=args.headless)
1043 if not result["success"]:
1044 raise RuntimeError(result["message"])
1045 print(f"Douyin login flow completed: {result['account_file']}")
1046 return 0
1047
1048 if args.action == "check":
1049 is_valid = await check_douyin_account(args.account)
1050 print("valid" if is_valid else "invalid")
1051 return 0 if is_valid else 1
1052
1053 publish_strategy = DOUYIN_PUBLISH_STRATEGY_SCHEDULED if args.schedule else DOUYIN_PUBLISH_STRATEGY_IMMEDIATE
1054
1055 if args.action == "upload-video":
1056 request = DouyinVideoUploadRequest(
1057 account_name=args.account,
1058 video_file=args.file,
1059 title=args.title,
1060 description=args.desc,
1061 tags=parse_tags(args.tags),
1062 publish_date=args.schedule or 0,
1063 thumbnail_file=args.thumbnail,
1064 thumbnail_landscape_file=args.thumbnail_landscape,
1065 thumbnail_portrait_file=args.thumbnail_portrait,
1066 product_link=args.product_link,
1067 product_title=args.product_title,
1068 declaration=args.declaration,
1069 publish_strategy=publish_strategy,
1070 debug=args.debug,
1071 headless=args.headless,
1072 collection_name=args.collection,
1073 )
1074 await upload_video(request)
1075 print(f"Douyin video upload submitted: {request.video_file}")
1076 return 0
1077
1078 if args.action == "upload-note":
1079 # 如果指定了 --notef,读取文件内容作为 note
1080 note_content = args.note
1081 if args.notef:
1082 note_file = Path(args.notef)
1083 if not note_file.exists():
1084 print(f"错误:文件不存在: {note_file}", file=sys.stderr)
1085 return 1
1086 note_content = note_file.read_text(encoding="utf-8")
1087
1088 request = DouyinNoteUploadRequest(
1089 account_name=args.account,
1090 image_files=parse_image_files(args.images),
1091 title=args.title,
1092 note=note_content,
1093 tags=parse_tags(args.tags),
1094 publish_date=args.schedule or 0,
1095 publish_strategy=publish_strategy,
1096 debug=args.debug,
1097 headless=args.headless,
1098 bgm=args.bgm or "",
1099 )
1100 await upload_note(request)
1101 print(f"Douyin note upload submitted: {len(request.image_files)} images")
1102 return 0
1103
1104 raise RuntimeError(f"Unsupported Douyin action: {args.action}")
1105
1106 if args.platform == "kuaishou":
1107 if args.action == "login":
1108 result = await login_kuaishou_account(args.account, headless=args.headless)
1109 if not result["success"]:
1110 raise RuntimeError(result["message"])
1111 print(f"Kuaishou login flow completed: {result['account_file']}")
1112 return 0
1113
1114 if args.action == "check":
1115 is_valid = await check_kuaishou_account(args.account)
1116 print("valid" if is_valid else "invalid")
1117 return 0 if is_valid else 1
1118
1119 publish_strategy = KUAISHOU_PUBLISH_STRATEGY_SCHEDULED if args.schedule else KUAISHOU_PUBLISH_STRATEGY_IMMEDIATE
1120
1121 if args.action == "upload-video":
1122 request = KuaishouVideoUploadRequest(
1123 account_name=args.account,
1124 video_file=args.file,
1125 title=args.title,
1126 description=args.desc,
1127 tags=parse_tags(args.tags),
1128 publish_date=args.schedule or 0,
1129 thumbnail_file=args.thumbnail,
1130 publish_strategy=publish_strategy,
1131 debug=args.debug,
1132 headless=args.headless,
1133 collection_name=args.collection,
1134 )
1135 await upload_kuaishou_video(request)
1136 print(f"Kuaishou video upload submitted: {request.video_file}")
1137 return 0
1138
1139 if args.action == "upload-note":
1140 request = KuaishouNoteUploadRequest(
1141 account_name=args.account,
1142 image_files=parse_image_files(args.images),
1143 title=args.title,
1144 note=args.note,
1145 tags=parse_tags(args.tags),
1146 publish_date=args.schedule or 0,
1147 publish_strategy=publish_strategy,
1148 debug=args.debug,
1149 headless=args.headless,
1150 )
1151 await upload_kuaishou_note(request)
1152 print(f"Kuaishou note upload submitted: {len(request.image_files)} images")
1153 return 0
1154
1155 raise RuntimeError(f"Unsupported Kuaishou action: {args.action}")
1156
1157 if args.platform == "xiaohongshu":
1158 if args.action == "login":
1159 result = await login_xiaohongshu_account(args.account, headless=args.headless)
1160 if not result["success"]:
1161 raise RuntimeError(result["message"])
1162 print(f"Xiaohongshu login flow completed: {result['account_file']}")
1163 return 0
1164
1165 if args.action == "check":
1166 is_valid = await check_xiaohongshu_account(args.account)
1167 print("valid" if is_valid else "invalid")
1168 return 0 if is_valid else 1
1169
1170 publish_strategy = (
1171 XIAOHONGSHU_PUBLISH_STRATEGY_SCHEDULED if args.schedule else XIAOHONGSHU_PUBLISH_STRATEGY_IMMEDIATE
1172 )
1173
1174 if args.action == "upload-video":
1175 parsed_tags = parse_tags(args.tags)
1176 if len(parsed_tags) > 10:
1177 print(f"错误:小红书标签最多 10 个,当前提供了 {len(parsed_tags)} 个: {parsed_tags}", file=sys.stderr)
1178 return 1
1179 request = XiaohongshuVideoUploadRequest(
1180 account_name=args.account,
1181 video_file=args.file,
1182 title=args.title,
1183 description=args.desc,
1184 tags=parsed_tags,
1185 publish_date=args.schedule or 0,
1186 thumbnail_file=args.thumbnail,
1187 publish_strategy=publish_strategy,
1188 debug=args.debug,
1189 headless=args.headless,
1190 )
1191 await upload_xiaohongshu_video(request)
1192 print(f"Xiaohongshu video upload submitted: {request.video_file}")
1193 return 0
1194
1195 if args.action == "upload-note":
1196 parsed_tags = parse_tags(args.tags)
1197 if len(parsed_tags) > 10:
1198 print(f"错误:小红书标签最多 10 个,当前提供了 {len(parsed_tags)} 个: {parsed_tags}", file=sys.stderr)
1199 return 1
1200 request = XiaohongshuNoteUploadRequest(
1201 account_name=args.account,
1202 image_files=parse_image_files(args.images),
1203 title=args.title,
1204 note=args.note,
1205 tags=parsed_tags,
1206 publish_date=args.schedule or 0,
1207 publish_strategy=publish_strategy,
1208 debug=args.debug,
1209 headless=args.headless,
1210 )
1211 await upload_xiaohongshu_note(request)
1212 print(f"Xiaohongshu note upload submitted: {len(request.image_files)} images")
1213 return 0
1214
1215 raise RuntimeError(f"Unsupported Xiaohongshu action: {args.action}")
1216
1217 if args.platform == "bilibili":
1218 if args.action == "login":
1219 result = await login_bilibili_account(args.account)
1220 if not result["success"]:
1221 raise RuntimeError(result["message"])
1222 print(f"Bilibili login flow completed: {result['account_file']}")
1223 return 0
1224
1225 if args.action == "check":
1226 is_valid = await check_bilibili_account(args.account)
1227 print("valid" if is_valid else "invalid")
1228 return 0 if is_valid else 1
1229
1230 if args.action == "upload-video":
1231 request = BilibiliVideoUploadRequest(
1232 account_name=args.account,
1233 video_file=args.file,
1234 title=args.title,
1235 description=args.desc,
1236 tid=args.tid,
1237 tags=parse_tags(args.tags),
1238 publish_date=args.schedule or 0,
1239 thumbnail_file=args.thumbnail,
1240 )
1241 await upload_bilibili_video(request)
1242 print(f"Bilibili video upload submitted: {request.video_file}")
1243 return 0
1244
1245 raise RuntimeError(f"Unsupported Bilibili action: {args.action}")
1246
1247 if args.platform == "tencent":
1248 if args.action == "login":
1249 result = await login_tencent_account(args.account, headless=args.headless)
1250 if not result["success"]:
1251 raise RuntimeError(result["message"])
1252 print(f"Tencent/WeChat Channels login flow completed: {result['account_file']}")
1253 return 0
1254
1255 if args.action == "check":
1256 is_valid = await check_tencent_account(args.account)
1257 print("valid" if is_valid else "invalid")
1258 return 0 if is_valid else 1
1259
1260 publish_strategy = TENCENT_PUBLISH_STRATEGY_SCHEDULED if args.schedule else TENCENT_PUBLISH_STRATEGY_IMMEDIATE
1261
1262 if args.action == "upload-video":
1263 request = TencentVideoUploadRequest(
1264 account_name=args.account,
1265 video_file=args.file,
1266 title=args.title,
1267 description=args.desc,
1268 tags=parse_tags(args.tags),
1269 publish_date=args.schedule or 0,
1270 thumbnail_file=args.thumbnail,
1271 thumbnail_landscape_file=args.thumbnail_landscape,
1272 thumbnail_portrait_file=args.thumbnail_portrait,
1273 short_title=args.short_title,
1274 category=args.category,
1275 is_draft=args.draft,
1276 publish_strategy=publish_strategy,
1277 debug=args.debug,
1278 headless=args.headless,
1279 collection_name=args.collection,
1280 )
1281 await upload_tencent_video(request)
1282 print(f"Tencent/WeChat Channels video upload submitted: {request.video_file}")
1283 return 0
1284
1285 raise RuntimeError(f"Unsupported Tencent/WeChat Channels action: {args.action}")
1286
1287 if args.platform == "alipay":
1288 if args.action == "login":
1289 result = await login_alipay_account(args.account, headless=args.headless)
1290 if not result["success"]:
1291 raise RuntimeError(result["message"])
1292 print(f"Alipay login flow completed: {result['account_file']}")
1293 return 0
1294
1295 if args.action == "check":
1296 is_valid = await check_alipay_account(args.account)
1297 print("valid" if is_valid else "invalid")
1298 return 0 if is_valid else 1
1299
1300 if args.action == "upload-video":
1301 request = AlipayVideoUploadRequest(
1302 account_name=args.account,
1303 video_file=args.file,
1304 title=args.title,
1305 description=args.desc,
1306 tags=parse_tags(args.tags),
1307 thumbnail_file=args.thumbnail,
1308 collection_name=args.collection,
1309 debug=args.debug,
1310 headless=args.headless,
1311 )
1312 await upload_alipay_video(request)
1313 print(f"Alipay video upload submitted: {request.video_file}")
1314 return 0
1315
1316 raise RuntimeError(f"Unsupported Alipay action: {args.action}")
1317
1318 if args.platform == "weibo":
1319 if args.action == "login":
1320 result = await login_weibo_account(args.account, headless=args.headless)
1321 if not result["success"]:
1322 raise RuntimeError(result["message"])
1323 print(f"Weibo login flow completed: {result['account_file']}")
1324 return 0
1325
1326 if args.action == "check":
1327 is_valid = await check_weibo_account(args.account)
1328 print("valid" if is_valid else "invalid")
1329 return 0 if is_valid else 1
1330
1331 if args.action == "upload-video":
1332 request = WeiboVideoUploadRequest(
1333 account_name=args.account,
1334 video_file=args.file,
1335 title=args.title,
1336 description=args.desc,
1337 tags=parse_tags(args.tags),
1338 thumbnail_file=args.thumbnail,
1339 collection_name=args.collection,
1340 debug=args.debug,
1341 headless=args.headless,
1342 )
1343 await upload_weibo_video(request)
1344 print(f"Weibo video upload submitted: {request.video_file}")
1345 return 0
1346
1347 raise RuntimeError(f"Unsupported Weibo action: {args.action}")
1348
1349 if args.platform == "hupu":
1350 if args.action == "login":
1351 result = await login_hupu_account(args.account, headless=args.headless)
1352 if not result["success"]:
1353 raise RuntimeError(result["message"])
1354 print(f"Hupu login flow completed: {result['account_file']}")
1355 return 0
1356
1357 if args.action == "check":
1358 is_valid = await check_hupu_account(args.account)
1359 print("valid" if is_valid else "invalid")
1360 return 0 if is_valid else 1
1361
1362 if args.action == "upload-video":
1363 request = HupuVideoUploadRequest(
1364 account_name=args.account,
1365 video_file=args.file,
1366 title=args.title,
1367 description=args.desc,
1368 tags=parse_tags(args.tags),
1369 thumbnail_file=args.thumbnail,
1370 debug=args.debug,
1371 headless=args.headless,
1372 )
1373 await upload_hupu_video(request)
1374 print(f"Hupu video upload submitted: {request.video_file}")
1375 return 0
1376
1377 raise RuntimeError(f"Unsupported Hupu action: {args.action}")
1378
1379 if args.platform == "youtube":
1380 if args.action == "login":
1381 result = await login_youtube_account(args.account, headless=args.headless)
1382 if not result["success"]:
1383 raise RuntimeError(result["message"])
1384 print(f"YouTube login flow completed: {result['account_file']}")
1385 return 0
1386
1387 if args.action == "check":
1388 is_valid = await check_youtube_account(args.account)
1389 print("valid" if is_valid else "invalid")
1390 return 0 if is_valid else 1
1391
1392 if args.action == "upload-video":
1393 request = YouTubeVideoUploadRequest(
1394 account_name=args.account,
1395 video_file=args.file,
1396 title=args.title,
1397 description=args.desc,
1398 tags=parse_tags(args.tags),
1399 thumbnail_file=args.thumbnail,
1400 playlist=args.playlist,
1401 visibility=args.visibility,
1402 debug=args.debug,
1403 headless=args.headless,
1404 )
1405 await upload_youtube_video(request)
1406 print(f"YouTube video upload submitted: {request.video_file}")
1407 return 0
1408
1409 raise RuntimeError(f"Unsupported YouTube action: {args.action}")
1410
1411 if args.platform == "baijiahao":
1412 if args.action == "login":
1413 result = await login_baijiahao_account(args.account, headless=args.headless)
1414 if not result["success"]:
1415 raise RuntimeError(result["message"])
1416 print(f"Baijiahao login flow completed: {result['account_file']}")
1417 return 0
1418
1419 if args.action == "check":
1420 is_valid = await check_baijiahao_account(args.account)
1421 print("valid" if is_valid else "invalid")
1422 return 0 if is_valid else 1
1423
1424 if args.action == "upload-video":
1425 request = BaijiahaoVideoUploadRequest(
1426 account_name=args.account,
1427 video_file=args.file,
1428 title=args.title,
1429 description=args.desc,
1430 tags=parse_tags(args.tags),
1431 thumbnail_file=args.thumbnail,
1432 collection_name=args.collection,
1433 debug=args.debug,
1434 headless=args.headless,
1435 )
1436 await upload_baijiahao_video(request)
1437 print(f"Baijiahao video upload submitted: {request.video_file}")
1438 return 0
1439
1440 raise RuntimeError(f"Unsupported Baijiahao action: {args.action}")
1441
1442 raise RuntimeError(f"Unsupported platform: {args.platform}")
1443
1444
1445 def main(argv: Sequence[str] | None = None) -> int:
1446 parser = build_parser()
1447 args = parser.parse_args(list(argv) if argv is not None else None)
1448 try:
1449 return asyncio.run(dispatch(args))
1450 except Exception as exc:
1451 print(str(exc), file=sys.stderr)
1452 return 1
1453
1454
1455 if __name__ == "__main__":
1456 raise SystemExit(main())
1457
1457 lines PYTHON