| 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 |