| 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.bilibili_uploader.runtime import run_biliup_command |
| 13 | from uploader.douyin_uploader.main import ( |
| 14 | DOUYIN_PUBLISH_STRATEGY_IMMEDIATE, |
| 15 | DOUYIN_PUBLISH_STRATEGY_SCHEDULED, |
| 16 | DouYinNote, |
| 17 | DouYinVideo, |
| 18 | cookie_auth as douyin_cookie_auth, |
| 19 | douyin_setup, |
| 20 | ) |
| 21 | from uploader.ks_uploader.main import ( |
| 22 | KUAISHOU_PUBLISH_STRATEGY_IMMEDIATE, |
| 23 | KUAISHOU_PUBLISH_STRATEGY_SCHEDULED, |
| 24 | KSNote, |
| 25 | KSVideo, |
| 26 | cookie_auth as kuaishou_cookie_auth, |
| 27 | ks_setup, |
| 28 | ) |
| 29 | from uploader.tencent_uploader.main import ( |
| 30 | TENCENT_PUBLISH_STRATEGY_IMMEDIATE, |
| 31 | TENCENT_PUBLISH_STRATEGY_SCHEDULED, |
| 32 | TencentVideo, |
| 33 | cookie_auth as tencent_cookie_auth, |
| 34 | tencent_setup, |
| 35 | ) |
| 36 | from uploader.xiaohongshu_uploader.main import ( |
| 37 | XIAOHONGSHU_PUBLISH_STRATEGY_IMMEDIATE, |
| 38 | XIAOHONGSHU_PUBLISH_STRATEGY_SCHEDULED, |
| 39 | XiaoHongShuNote, |
| 40 | XiaoHongShuVideo, |
| 41 | cookie_auth as xiaohongshu_cookie_auth, |
| 42 | xiaohongshu_setup, |
| 43 | ) |
| 44 | from uploader.youtube_uploader.main import ( |
| 45 | YouTubeVideo, |
| 46 | cookie_auth as youtube_cookie_auth, |
| 47 | youtube_setup, |
| 48 | ) |
| 49 | |
| 50 | SCHEDULE_FORMAT = "%Y-%m-%d %H:%M" |
| 51 | |
| 52 | |
| 53 | @dataclass(slots=True) |
| 54 | class DouyinVideoUploadRequest: |
| 55 | account_name: str |
| 56 | video_file: Path |
| 57 | title: str |
| 58 | description: str |
| 59 | tags: list[str] |
| 60 | publish_date: datetime | int |
| 61 | thumbnail_file: Path | None = None |
| 62 | thumbnail_landscape_file: Path | None = None |
| 63 | thumbnail_portrait_file: Path | None = None |
| 64 | product_link: str = "" |
| 65 | product_title: str = "" |
| 66 | publish_strategy: str = DOUYIN_PUBLISH_STRATEGY_IMMEDIATE |
| 67 | debug: bool = True |
| 68 | headless: bool = True |
| 69 | declaration: str | None = None |
| 70 | |
| 71 | |
| 72 | @dataclass(slots=True) |
| 73 | class DouyinNoteUploadRequest: |
| 74 | account_name: str |
| 75 | image_files: list[Path] |
| 76 | title: str |
| 77 | note: str |
| 78 | tags: list[str] |
| 79 | publish_date: datetime | int |
| 80 | publish_strategy: str = DOUYIN_PUBLISH_STRATEGY_IMMEDIATE |
| 81 | debug: bool = True |
| 82 | headless: bool = True |
| 83 | bgm: str = "" |
| 84 | |
| 85 | |
| 86 | @dataclass(slots=True) |
| 87 | class KuaishouVideoUploadRequest: |
| 88 | account_name: str |
| 89 | video_file: Path |
| 90 | title: str |
| 91 | description: str |
| 92 | tags: list[str] |
| 93 | publish_date: datetime | int |
| 94 | thumbnail_file: Path | None = None |
| 95 | publish_strategy: str = KUAISHOU_PUBLISH_STRATEGY_IMMEDIATE |
| 96 | debug: bool = True |
| 97 | headless: bool = True |
| 98 | |
| 99 | |
| 100 | @dataclass(slots=True) |
| 101 | class KuaishouNoteUploadRequest: |
| 102 | account_name: str |
| 103 | image_files: list[Path] |
| 104 | title: str |
| 105 | note: str |
| 106 | tags: list[str] |
| 107 | publish_date: datetime | int |
| 108 | publish_strategy: str = KUAISHOU_PUBLISH_STRATEGY_IMMEDIATE |
| 109 | debug: bool = True |
| 110 | headless: bool = True |
| 111 | |
| 112 | |
| 113 | @dataclass(slots=True) |
| 114 | class XiaohongshuVideoUploadRequest: |
| 115 | account_name: str |
| 116 | video_file: Path |
| 117 | title: str |
| 118 | description: str |
| 119 | tags: list[str] |
| 120 | publish_date: datetime | int |
| 121 | thumbnail_file: Path | None = None |
| 122 | publish_strategy: str = XIAOHONGSHU_PUBLISH_STRATEGY_IMMEDIATE |
| 123 | debug: bool = True |
| 124 | headless: bool = True |
| 125 | |
| 126 | |
| 127 | @dataclass(slots=True) |
| 128 | class XiaohongshuNoteUploadRequest: |
| 129 | account_name: str |
| 130 | image_files: list[Path] |
| 131 | title: str |
| 132 | note: str |
| 133 | tags: list[str] |
| 134 | publish_date: datetime | int |
| 135 | publish_strategy: str = XIAOHONGSHU_PUBLISH_STRATEGY_IMMEDIATE |
| 136 | debug: bool = True |
| 137 | headless: bool = True |
| 138 | |
| 139 | |
| 140 | @dataclass(slots=True) |
| 141 | class BilibiliVideoUploadRequest: |
| 142 | account_name: str |
| 143 | video_file: Path |
| 144 | title: str |
| 145 | description: str |
| 146 | tid: int |
| 147 | tags: list[str] |
| 148 | publish_date: datetime | int |
| 149 | thumbnail_file: Path | None = None |
| 150 | |
| 151 | |
| 152 | @dataclass(slots=True) |
| 153 | class TencentVideoUploadRequest: |
| 154 | account_name: str |
| 155 | video_file: Path |
| 156 | title: str |
| 157 | description: str |
| 158 | tags: list[str] |
| 159 | publish_date: datetime | int |
| 160 | thumbnail_file: Path | None = None |
| 161 | thumbnail_landscape_file: Path | None = None |
| 162 | thumbnail_portrait_file: Path | None = None |
| 163 | short_title: str | None = None |
| 164 | category: str | None = None |
| 165 | is_draft: bool = False |
| 166 | publish_strategy: str = TENCENT_PUBLISH_STRATEGY_IMMEDIATE |
| 167 | debug: bool = True |
| 168 | headless: bool = True |
| 169 | |
| 170 | |
| 171 | @dataclass(slots=True) |
| 172 | class YouTubeVideoUploadRequest: |
| 173 | account_name: str |
| 174 | video_file: Path |
| 175 | title: str |
| 176 | description: str |
| 177 | tags: list[str] |
| 178 | thumbnail_file: Path | None = None |
| 179 | playlist: str | None = None |
| 180 | visibility: str = "public" |
| 181 | debug: bool = True |
| 182 | headless: bool = False |
| 183 | |
| 184 | |
| 185 | def has_interactive_terminal() -> bool: |
| 186 | return sys.stdin.isatty() and sys.stdout.isatty() |
| 187 | |
| 188 | |
| 189 | def resolve_runtime_home() -> Path: |
| 190 | return Path(BASE_DIR) |
| 191 | |
| 192 | |
| 193 | def resolve_account_file(platform: str, account_name: str) -> Path: |
| 194 | account_file = resolve_runtime_home() / "cookies" / f"{platform}_{account_name}.json" |
| 195 | account_file.parent.mkdir(exist_ok=True) |
| 196 | return account_file |
| 197 | |
| 198 | |
| 199 | def parse_tags(raw_tags: str | None) -> list[str]: |
| 200 | if not raw_tags: |
| 201 | return [] |
| 202 | |
| 203 | tags: list[str] = [] |
| 204 | for item in raw_tags.split(","): |
| 205 | cleaned = item.strip().lstrip("#") |
| 206 | if cleaned: |
| 207 | tags.append(cleaned) |
| 208 | return tags |
| 209 | |
| 210 | |
| 211 | def parse_image_files(raw_files: Iterable[Path]) -> list[Path]: |
| 212 | return [Path(file) for file in raw_files] |
| 213 | |
| 214 | |
| 215 | def parse_schedule(raw_schedule: str | None) -> datetime | int: |
| 216 | if not raw_schedule: |
| 217 | return 0 |
| 218 | return datetime.strptime(raw_schedule, SCHEDULE_FORMAT) |
| 219 | |
| 220 | |
| 221 | async def login_douyin_account(account_name: str, headless: bool = True) -> dict: |
| 222 | account_file = resolve_account_file("douyin", account_name) |
| 223 | return await douyin_setup(str(account_file), handle=True, return_detail=True, headless=headless) |
| 224 | |
| 225 | |
| 226 | async def check_douyin_account(account_name: str) -> bool: |
| 227 | account_file = resolve_account_file("douyin", account_name) |
| 228 | if not account_file.exists(): |
| 229 | return False |
| 230 | return await douyin_cookie_auth(str(account_file)) |
| 231 | |
| 232 | |
| 233 | async def login_kuaishou_account(account_name: str, headless: bool = True) -> dict: |
| 234 | account_file = resolve_account_file("kuaishou", account_name) |
| 235 | return await ks_setup(str(account_file), handle=True, return_detail=True, headless=headless) |
| 236 | |
| 237 | |
| 238 | async def check_kuaishou_account(account_name: str) -> bool: |
| 239 | account_file = resolve_account_file("kuaishou", account_name) |
| 240 | if not account_file.exists(): |
| 241 | return False |
| 242 | return await kuaishou_cookie_auth(str(account_file)) |
| 243 | |
| 244 | |
| 245 | async def login_xiaohongshu_account(account_name: str, headless: bool = True) -> dict: |
| 246 | account_file = resolve_account_file("xiaohongshu", account_name) |
| 247 | return await xiaohongshu_setup(str(account_file), handle=True, return_detail=True, headless=headless) |
| 248 | |
| 249 | |
| 250 | async def check_xiaohongshu_account(account_name: str) -> bool: |
| 251 | account_file = resolve_account_file("xiaohongshu", account_name) |
| 252 | if not account_file.exists(): |
| 253 | return False |
| 254 | return await xiaohongshu_cookie_auth(str(account_file)) |
| 255 | |
| 256 | |
| 257 | async def login_bilibili_account(account_name: str) -> dict: |
| 258 | account_file = resolve_account_file("bilibili", account_name) |
| 259 | if not has_interactive_terminal(): |
| 260 | return { |
| 261 | "success": False, |
| 262 | "message": ( |
| 263 | "Bilibili login requires a local interactive terminal. " |
| 264 | f"Please run `sau bilibili login --account {account_name}` yourself in a local terminal. " |
| 265 | "If the terminal QR code does not render completely, open `./qrcode.png` and scan that image." |
| 266 | ), |
| 267 | "account_file": str(account_file), |
| 268 | } |
| 269 | |
| 270 | result = run_biliup_command(["-u", str(account_file), "login"], interactive=True) |
| 271 | success = result.returncode == 0 |
| 272 | return { |
| 273 | "success": success, |
| 274 | "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", |
| 275 | "account_file": str(account_file), |
| 276 | } |
| 277 | |
| 278 | |
| 279 | async def check_bilibili_account(account_name: str) -> bool: |
| 280 | account_file = resolve_account_file("bilibili", account_name) |
| 281 | if not account_file.exists(): |
| 282 | return False |
| 283 | result = run_biliup_command(["-u", str(account_file), "renew"]) |
| 284 | return result.returncode == 0 |
| 285 | |
| 286 | |
| 287 | async def login_tencent_account(account_name: str, headless: bool = True) -> dict: |
| 288 | account_file = resolve_account_file("tencent", account_name) |
| 289 | return await tencent_setup(str(account_file), handle=True, return_detail=True, headless=headless) |
| 290 | |
| 291 | |
| 292 | async def check_tencent_account(account_name: str) -> bool: |
| 293 | account_file = resolve_account_file("tencent", account_name) |
| 294 | if not account_file.exists(): |
| 295 | return False |
| 296 | return await tencent_cookie_auth(str(account_file)) |
| 297 | |
| 298 | |
| 299 | async def login_youtube_account(account_name: str, headless: bool = False) -> dict: |
| 300 | account_file = resolve_account_file("youtube", account_name) |
| 301 | return await youtube_setup(str(account_file), handle=True, return_detail=True, headless=headless) |
| 302 | |
| 303 | |
| 304 | async def check_youtube_account(account_name: str) -> bool: |
| 305 | account_file = resolve_account_file("youtube", account_name) |
| 306 | if not account_file.exists(): |
| 307 | return False |
| 308 | return await youtube_cookie_auth(str(account_file)) |
| 309 | |
| 310 | |
| 311 | async def upload_youtube_video(request: YouTubeVideoUploadRequest) -> Path: |
| 312 | account_file = resolve_account_file("youtube", request.account_name) |
| 313 | is_ready = await youtube_setup(str(account_file), handle=False) |
| 314 | if not is_ready: |
| 315 | raise RuntimeError( |
| 316 | f"YouTube cookie is missing or expired: {account_file}. Run `sau youtube login --account {request.account_name}` first." |
| 317 | ) |
| 318 | |
| 319 | app = YouTubeVideo( |
| 320 | request.title, |
| 321 | str(request.video_file), |
| 322 | request.tags, |
| 323 | str(account_file), |
| 324 | description=request.description, |
| 325 | thumbnail_path=str(request.thumbnail_file) if request.thumbnail_file else None, |
| 326 | playlist=request.playlist, |
| 327 | visibility=request.visibility, |
| 328 | debug=request.debug, |
| 329 | headless=request.headless, |
| 330 | ) |
| 331 | await app.main() |
| 332 | return account_file |
| 333 | |
| 334 | |
| 335 | async def upload_video(request: DouyinVideoUploadRequest) -> Path: |
| 336 | account_file = resolve_account_file("douyin", request.account_name) |
| 337 | is_ready = await douyin_setup(str(account_file), handle=False) |
| 338 | if not is_ready: |
| 339 | raise RuntimeError( |
| 340 | f"Douyin cookie is missing or expired: {account_file}. Run `sau douyin login --account {request.account_name}` first." |
| 341 | ) |
| 342 | |
| 343 | app = DouYinVideo( |
| 344 | request.title, |
| 345 | str(request.video_file), |
| 346 | request.tags, |
| 347 | request.publish_date, |
| 348 | str(account_file), |
| 349 | desc=request.description, |
| 350 | thumbnail_landscape_path=( |
| 351 | str(request.thumbnail_landscape_file) if request.thumbnail_landscape_file else None |
| 352 | ), |
| 353 | thumbnail_portrait_path=str( |
| 354 | request.thumbnail_portrait_file or request.thumbnail_file |
| 355 | ) if request.thumbnail_portrait_file or request.thumbnail_file else None, |
| 356 | productLink=request.product_link, |
| 357 | productTitle=request.product_title, |
| 358 | declaration=request.declaration, |
| 359 | publish_strategy=request.publish_strategy, |
| 360 | debug=request.debug, |
| 361 | headless=request.headless, |
| 362 | ) |
| 363 | await app.douyin_upload_video() |
| 364 | return account_file |
| 365 | |
| 366 | |
| 367 | async def upload_note(request: DouyinNoteUploadRequest) -> Path: |
| 368 | account_file = resolve_account_file("douyin", request.account_name) |
| 369 | is_ready = await douyin_setup(str(account_file), handle=False) |
| 370 | if not is_ready: |
| 371 | raise RuntimeError( |
| 372 | f"Douyin cookie is missing or expired: {account_file}. Run `sau douyin login --account {request.account_name}` first." |
| 373 | ) |
| 374 | |
| 375 | app = DouYinNote( |
| 376 | image_paths=[str(path) for path in request.image_files], |
| 377 | title=request.title, |
| 378 | note=request.note, |
| 379 | tags=request.tags, |
| 380 | publish_date=request.publish_date, |
| 381 | account_file=str(account_file), |
| 382 | publish_strategy=request.publish_strategy, |
| 383 | debug=request.debug, |
| 384 | headless=request.headless, |
| 385 | bgm=request.bgm, |
| 386 | ) |
| 387 | await app.douyin_upload_note() |
| 388 | return account_file |
| 389 | |
| 390 | |
| 391 | async def upload_kuaishou_video(request: KuaishouVideoUploadRequest) -> Path: |
| 392 | account_file = resolve_account_file("kuaishou", request.account_name) |
| 393 | is_ready = await ks_setup(str(account_file), handle=False) |
| 394 | if not is_ready: |
| 395 | raise RuntimeError( |
| 396 | f"Kuaishou cookie is missing or expired: {account_file}. Run `sau kuaishou login --account {request.account_name}` first." |
| 397 | ) |
| 398 | |
| 399 | app = KSVideo( |
| 400 | title=request.title, |
| 401 | file_path=str(request.video_file), |
| 402 | desc=request.description, |
| 403 | tags=request.tags, |
| 404 | publish_date=request.publish_date, |
| 405 | account_file=str(account_file), |
| 406 | thumbnail_path=str(request.thumbnail_file) if request.thumbnail_file else None, |
| 407 | publish_strategy=request.publish_strategy, |
| 408 | debug=request.debug, |
| 409 | headless=request.headless, |
| 410 | ) |
| 411 | await app.main() |
| 412 | return account_file |
| 413 | |
| 414 | |
| 415 | async def upload_kuaishou_note(request: KuaishouNoteUploadRequest) -> Path: |
| 416 | account_file = resolve_account_file("kuaishou", request.account_name) |
| 417 | is_ready = await ks_setup(str(account_file), handle=False) |
| 418 | if not is_ready: |
| 419 | raise RuntimeError( |
| 420 | f"Kuaishou cookie is missing or expired: {account_file}. Run `sau kuaishou login --account {request.account_name}` first." |
| 421 | ) |
| 422 | |
| 423 | app = KSNote( |
| 424 | image_paths=[str(path) for path in request.image_files], |
| 425 | title=request.title, |
| 426 | note=request.note, |
| 427 | tags=request.tags, |
| 428 | publish_date=request.publish_date, |
| 429 | account_file=str(account_file), |
| 430 | publish_strategy=request.publish_strategy, |
| 431 | debug=request.debug, |
| 432 | headless=request.headless, |
| 433 | ) |
| 434 | await app.main() |
| 435 | return account_file |
| 436 | |
| 437 | |
| 438 | async def upload_xiaohongshu_video(request: XiaohongshuVideoUploadRequest) -> Path: |
| 439 | account_file = resolve_account_file("xiaohongshu", request.account_name) |
| 440 | is_ready = await xiaohongshu_setup(str(account_file), handle=False) |
| 441 | if not is_ready: |
| 442 | raise RuntimeError( |
| 443 | f"Xiaohongshu cookie is missing or expired: {account_file}. Run `sau xiaohongshu login --account {request.account_name}` first." |
| 444 | ) |
| 445 | |
| 446 | app = XiaoHongShuVideo( |
| 447 | title=request.title, |
| 448 | file_path=str(request.video_file), |
| 449 | desc=request.description, |
| 450 | tags=request.tags, |
| 451 | publish_date=request.publish_date, |
| 452 | account_file=str(account_file), |
| 453 | thumbnail_path=str(request.thumbnail_file) if request.thumbnail_file else None, |
| 454 | publish_strategy=request.publish_strategy, |
| 455 | debug=request.debug, |
| 456 | headless=request.headless, |
| 457 | ) |
| 458 | await app.main() |
| 459 | return account_file |
| 460 | |
| 461 | |
| 462 | async def upload_xiaohongshu_note(request: XiaohongshuNoteUploadRequest) -> Path: |
| 463 | account_file = resolve_account_file("xiaohongshu", request.account_name) |
| 464 | is_ready = await xiaohongshu_setup(str(account_file), handle=False) |
| 465 | if not is_ready: |
| 466 | raise RuntimeError( |
| 467 | f"Xiaohongshu cookie is missing or expired: {account_file}. Run `sau xiaohongshu login --account {request.account_name}` first." |
| 468 | ) |
| 469 | |
| 470 | app = XiaoHongShuNote( |
| 471 | image_paths=[str(path) for path in request.image_files], |
| 472 | title=request.title, |
| 473 | desc=request.note, |
| 474 | note=request.note, |
| 475 | tags=request.tags, |
| 476 | publish_date=request.publish_date, |
| 477 | account_file=str(account_file), |
| 478 | publish_strategy=request.publish_strategy, |
| 479 | debug=request.debug, |
| 480 | headless=request.headless, |
| 481 | ) |
| 482 | await app.main() |
| 483 | return account_file |
| 484 | |
| 485 | |
| 486 | async def upload_bilibili_video(request: BilibiliVideoUploadRequest) -> Path: |
| 487 | account_file = resolve_account_file("bilibili", request.account_name) |
| 488 | if not account_file.exists(): |
| 489 | raise RuntimeError( |
| 490 | f"Bilibili account file is missing: {account_file}. Run `sau bilibili login --account {request.account_name}` first." |
| 491 | ) |
| 492 | |
| 493 | arguments = [ |
| 494 | "-u", |
| 495 | str(account_file), |
| 496 | "upload", |
| 497 | str(request.video_file), |
| 498 | "--title", |
| 499 | request.title, |
| 500 | "--desc", |
| 501 | request.description, |
| 502 | "--tid", |
| 503 | str(request.tid), |
| 504 | ] |
| 505 | if request.tags: |
| 506 | arguments.extend(["--tag", ",".join(request.tags)]) |
| 507 | if request.thumbnail_file: |
| 508 | arguments.extend(["--cover", str(request.thumbnail_file)]) |
| 509 | if isinstance(request.publish_date, datetime): |
| 510 | arguments.extend(["--dtime", str(int(request.publish_date.timestamp()))]) |
| 511 | |
| 512 | result = run_biliup_command(arguments) |
| 513 | if result.returncode != 0: |
| 514 | raise RuntimeError((result.stderr or result.stdout or "").strip() or "Bilibili upload failed") |
| 515 | return account_file |
| 516 | |
| 517 | |
| 518 | async def upload_tencent_video(request: TencentVideoUploadRequest) -> Path: |
| 519 | account_file = resolve_account_file("tencent", request.account_name) |
| 520 | is_ready = await tencent_setup(str(account_file), handle=False) |
| 521 | if not is_ready: |
| 522 | raise RuntimeError( |
| 523 | f"Tencent/WeChat Channels cookie is missing or expired: {account_file}. " |
| 524 | f"Run `sau tencent login --account {request.account_name}` first." |
| 525 | ) |
| 526 | |
| 527 | app = TencentVideo( |
| 528 | title=request.title, |
| 529 | file_path=str(request.video_file), |
| 530 | tags=request.tags, |
| 531 | publish_date=request.publish_date, |
| 532 | account_file=str(account_file), |
| 533 | category=request.category, |
| 534 | is_draft=request.is_draft, |
| 535 | desc=request.description, |
| 536 | thumbnail_path=str(request.thumbnail_file) if request.thumbnail_file else None, |
| 537 | thumbnail_landscape_path=( |
| 538 | str(request.thumbnail_landscape_file) if request.thumbnail_landscape_file else None |
| 539 | ), |
| 540 | thumbnail_portrait_path=( |
| 541 | str(request.thumbnail_portrait_file) if request.thumbnail_portrait_file else None |
| 542 | ), |
| 543 | short_title=request.short_title, |
| 544 | publish_strategy=request.publish_strategy, |
| 545 | debug=request.debug, |
| 546 | headless=request.headless, |
| 547 | ) |
| 548 | await app.tencent_upload_video() |
| 549 | return account_file |
| 550 | |
| 551 | |
| 552 | def existing_file_path(value: str) -> Path: |
| 553 | path = Path(value) |
| 554 | if not path.is_file(): |
| 555 | raise argparse.ArgumentTypeError(f"File not found: {value}") |
| 556 | return path |
| 557 | |
| 558 | |
| 559 | def schedule_value(value: str): |
| 560 | try: |
| 561 | return parse_schedule(value) |
| 562 | except ValueError as exc: |
| 563 | raise argparse.ArgumentTypeError( |
| 564 | f"Invalid schedule '{value}'. Expected format: {SCHEDULE_FORMAT}" |
| 565 | ) from exc |
| 566 | |
| 567 | |
| 568 | def add_runtime_flags(parser: argparse.ArgumentParser) -> None: |
| 569 | parser.add_argument("--debug", action="store_true", help="Enable debug mode") |
| 570 | headless_group = parser.add_mutually_exclusive_group() |
| 571 | headless_group.add_argument("--headed", dest="headless", action="store_false", help="Run with browser UI") |
| 572 | headless_group.add_argument("--headless", dest="headless", action="store_true", help="Run in headless mode") |
| 573 | parser.set_defaults(headless=True) |
| 574 | |
| 575 | |
| 576 | def build_parser() -> argparse.ArgumentParser: |
| 577 | schedule_help = SCHEDULE_FORMAT.replace("%", "%%") |
| 578 | parser = argparse.ArgumentParser( |
| 579 | prog="sau", |
| 580 | description="CLI for social-auto-upload.", |
| 581 | ) |
| 582 | platform_parsers = parser.add_subparsers(dest="platform", required=True) |
| 583 | |
| 584 | douyin_parser = platform_parsers.add_parser("douyin", help="Douyin operations") |
| 585 | douyin_actions = douyin_parser.add_subparsers(dest="action", required=True) |
| 586 | |
| 587 | for action_name in ("login", "check"): |
| 588 | action_parser = douyin_actions.add_parser(action_name, help=f"Douyin {action_name}") |
| 589 | action_parser.add_argument("--account", required=True, help="Douyin user-defined account_name") |
| 590 | if action_name == "login": |
| 591 | add_runtime_flags(action_parser) |
| 592 | |
| 593 | upload_video_parser = douyin_actions.add_parser("upload-video", help="Upload one video to Douyin") |
| 594 | upload_video_parser.add_argument("--account", required=True, help="Douyin user-defined account_name") |
| 595 | upload_video_parser.add_argument("--file", required=True, type=existing_file_path, help="Video file path") |
| 596 | upload_video_parser.add_argument("--title", required=True, help="Video title") |
| 597 | upload_video_parser.add_argument("--desc", default="", help="Optional video description") |
| 598 | upload_video_parser.add_argument("--tags", default="", help="Comma-separated tags, such as tag1,tag2") |
| 599 | upload_video_parser.add_argument("--schedule", type=schedule_value, help=f"Schedule time in {schedule_help}") |
| 600 | upload_video_parser.add_argument("--thumbnail", type=existing_file_path, help="Optional 3:4 portrait thumbnail path") |
| 601 | upload_video_parser.add_argument("--thumbnail-landscape", type=existing_file_path, help="Optional 4:3 landscape thumbnail path") |
| 602 | upload_video_parser.add_argument("--thumbnail-portrait", type=existing_file_path, help="Optional 3:4 portrait thumbnail path") |
| 603 | upload_video_parser.add_argument("--product-link", default="", help="Optional product link") |
| 604 | upload_video_parser.add_argument("--product-title", default="", help="Optional product title") |
| 605 | upload_video_parser.add_argument( |
| 606 | "--declaration", |
| 607 | help="Exact Douyin self-declaration option text; omitted means do not set one", |
| 608 | ) |
| 609 | add_runtime_flags(upload_video_parser) |
| 610 | |
| 611 | upload_note_parser = douyin_actions.add_parser("upload-note", help="Upload one note to Douyin") |
| 612 | upload_note_parser.add_argument("--account", required=True, help="Douyin user-defined account_name") |
| 613 | upload_note_parser.add_argument("--images", required=True, nargs="+", type=existing_file_path, help="Image file paths") |
| 614 | upload_note_parser.add_argument("--title", required=True, help="Note title") |
| 615 | upload_note_parser.add_argument("--note", default="", help="Optional note content") |
| 616 | upload_note_parser.add_argument("--notef", default="", help="Read note content from file (txt/md)") |
| 617 | upload_note_parser.add_argument("--tags", default="", help="Comma-separated tags, such as tag1,tag2") |
| 618 | upload_note_parser.add_argument("--bgm", default="", help="BGM music name to search and select") |
| 619 | upload_note_parser.add_argument("--schedule", type=schedule_value, help=f"Schedule time in {schedule_help}") |
| 620 | add_runtime_flags(upload_note_parser) |
| 621 | |
| 622 | kuaishou_parser = platform_parsers.add_parser("kuaishou", help="Kuaishou operations") |
| 623 | kuaishou_actions = kuaishou_parser.add_subparsers(dest="action", required=True) |
| 624 | |
| 625 | for action_name in ("login", "check"): |
| 626 | action_parser = kuaishou_actions.add_parser(action_name, help=f"Kuaishou {action_name}") |
| 627 | action_parser.add_argument("--account", required=True, help="Kuaishou user-defined account_name") |
| 628 | if action_name == "login": |
| 629 | add_runtime_flags(action_parser) |
| 630 | |
| 631 | kuaishou_upload_video_parser = kuaishou_actions.add_parser("upload-video", help="Upload one video to Kuaishou") |
| 632 | kuaishou_upload_video_parser.add_argument("--account", required=True, help="Kuaishou user-defined account_name") |
| 633 | kuaishou_upload_video_parser.add_argument("--file", required=True, type=existing_file_path, help="Video file path") |
| 634 | kuaishou_upload_video_parser.add_argument("--title", required=True, help="Video title") |
| 635 | kuaishou_upload_video_parser.add_argument("--desc", default="", help="Optional video description") |
| 636 | kuaishou_upload_video_parser.add_argument("--tags", default="", help="Comma-separated tags, such as tag1,tag2") |
| 637 | kuaishou_upload_video_parser.add_argument("--schedule", type=schedule_value, help=f"Schedule time in {schedule_help}") |
| 638 | kuaishou_upload_video_parser.add_argument("--thumbnail", type=existing_file_path, help="Optional thumbnail path") |
| 639 | add_runtime_flags(kuaishou_upload_video_parser) |
| 640 | |
| 641 | kuaishou_upload_note_parser = kuaishou_actions.add_parser("upload-note", help="Upload one note to Kuaishou") |
| 642 | kuaishou_upload_note_parser.add_argument("--account", required=True, help="Kuaishou user-defined account_name") |
| 643 | kuaishou_upload_note_parser.add_argument("--images", required=True, nargs="+", type=existing_file_path, help="Image file paths") |
| 644 | kuaishou_upload_note_parser.add_argument("--title", required=True, help="Note title") |
| 645 | kuaishou_upload_note_parser.add_argument("--note", default="", help="Optional note content") |
| 646 | kuaishou_upload_note_parser.add_argument("--tags", default="", help="Comma-separated tags, such as tag1,tag2") |
| 647 | kuaishou_upload_note_parser.add_argument("--schedule", type=schedule_value, help=f"Schedule time in {schedule_help}") |
| 648 | add_runtime_flags(kuaishou_upload_note_parser) |
| 649 | |
| 650 | xiaohongshu_parser = platform_parsers.add_parser("xiaohongshu", help="Xiaohongshu operations") |
| 651 | xiaohongshu_actions = xiaohongshu_parser.add_subparsers(dest="action", required=True) |
| 652 | |
| 653 | for action_name in ("login", "check"): |
| 654 | action_parser = xiaohongshu_actions.add_parser(action_name, help=f"Xiaohongshu {action_name}") |
| 655 | action_parser.add_argument("--account", required=True, help="Xiaohongshu user-defined account_name") |
| 656 | if action_name == "login": |
| 657 | add_runtime_flags(action_parser) |
| 658 | |
| 659 | xiaohongshu_upload_video_parser = xiaohongshu_actions.add_parser("upload-video", help="Upload one video to Xiaohongshu") |
| 660 | xiaohongshu_upload_video_parser.add_argument("--account", required=True, help="Xiaohongshu user-defined account_name") |
| 661 | xiaohongshu_upload_video_parser.add_argument("--file", required=True, type=existing_file_path, help="Video file path") |
| 662 | xiaohongshu_upload_video_parser.add_argument("--title", required=True, help="Video title") |
| 663 | xiaohongshu_upload_video_parser.add_argument("--desc", default="", help="Optional video description") |
| 664 | xiaohongshu_upload_video_parser.add_argument("--tags", default="", help="Comma-separated tags, such as tag1,tag2") |
| 665 | xiaohongshu_upload_video_parser.add_argument("--schedule", type=schedule_value, help=f"Schedule time in {schedule_help}") |
| 666 | xiaohongshu_upload_video_parser.add_argument("--thumbnail", type=existing_file_path, help="Optional thumbnail path") |
| 667 | add_runtime_flags(xiaohongshu_upload_video_parser) |
| 668 | |
| 669 | xiaohongshu_upload_note_parser = xiaohongshu_actions.add_parser("upload-note", help="Upload one note to Xiaohongshu") |
| 670 | xiaohongshu_upload_note_parser.add_argument("--account", required=True, help="Xiaohongshu user-defined account_name") |
| 671 | xiaohongshu_upload_note_parser.add_argument("--images", required=True, nargs="+", type=existing_file_path, help="Image file paths") |
| 672 | xiaohongshu_upload_note_parser.add_argument("--title", required=True, help="Note title") |
| 673 | xiaohongshu_upload_note_parser.add_argument("--note", default="", help="Optional note content") |
| 674 | xiaohongshu_upload_note_parser.add_argument("--tags", default="", help="Comma-separated tags, such as tag1,tag2") |
| 675 | xiaohongshu_upload_note_parser.add_argument("--schedule", type=schedule_value, help=f"Schedule time in {schedule_help}") |
| 676 | add_runtime_flags(xiaohongshu_upload_note_parser) |
| 677 | |
| 678 | bilibili_parser = platform_parsers.add_parser("bilibili", help="Bilibili operations") |
| 679 | bilibili_actions = bilibili_parser.add_subparsers(dest="action", required=True) |
| 680 | |
| 681 | for action_name in ("login", "check"): |
| 682 | action_parser = bilibili_actions.add_parser(action_name, help=f"Bilibili {action_name}") |
| 683 | action_parser.add_argument("--account", required=True, help="Bilibili user-defined account_name") |
| 684 | |
| 685 | bilibili_upload_video_parser = bilibili_actions.add_parser("upload-video", help="Upload one video to Bilibili") |
| 686 | bilibili_upload_video_parser.add_argument("--account", required=True, help="Bilibili user-defined account_name") |
| 687 | bilibili_upload_video_parser.add_argument("--file", required=True, type=existing_file_path, help="Video file path") |
| 688 | bilibili_upload_video_parser.add_argument("--title", required=True, help="Video title") |
| 689 | bilibili_upload_video_parser.add_argument("--desc", required=True, help="Video description") |
| 690 | bilibili_upload_video_parser.add_argument("--tid", required=True, type=int, help="Bilibili category id") |
| 691 | bilibili_upload_video_parser.add_argument("--tags", default="", help="Comma-separated tags, such as tag1,tag2") |
| 692 | bilibili_upload_video_parser.add_argument("--thumbnail", type=existing_file_path, help="Optional Bilibili cover image path") |
| 693 | bilibili_upload_video_parser.add_argument("--schedule", type=schedule_value, help=f"Schedule time in {schedule_help}") |
| 694 | |
| 695 | tencent_parser = platform_parsers.add_parser("tencent", help="Tencent/WeChat Channels operations") |
| 696 | tencent_actions = tencent_parser.add_subparsers(dest="action", required=True) |
| 697 | |
| 698 | for action_name in ("login", "check"): |
| 699 | action_parser = tencent_actions.add_parser(action_name, help=f"Tencent/WeChat Channels {action_name}") |
| 700 | action_parser.add_argument("--account", required=True, help="Tencent user-defined account_name") |
| 701 | if action_name == "login": |
| 702 | add_runtime_flags(action_parser) |
| 703 | |
| 704 | tencent_upload_video_parser = tencent_actions.add_parser("upload-video", help="Upload one video to WeChat Channels") |
| 705 | tencent_upload_video_parser.add_argument("--account", required=True, help="Tencent user-defined account_name") |
| 706 | tencent_upload_video_parser.add_argument("--file", required=True, type=existing_file_path, help="Video file path") |
| 707 | tencent_upload_video_parser.add_argument("--title", required=True, help="Video title") |
| 708 | tencent_upload_video_parser.add_argument("--desc", default="", help="Optional video description") |
| 709 | tencent_upload_video_parser.add_argument("--tags", default="", help="Comma-separated tags, such as tag1,tag2") |
| 710 | tencent_upload_video_parser.add_argument("--schedule", type=schedule_value, help=f"Schedule time in {schedule_help}") |
| 711 | tencent_upload_video_parser.add_argument("--thumbnail", type=existing_file_path, help="Optional 3:4 portrait thumbnail path") |
| 712 | tencent_upload_video_parser.add_argument("--thumbnail-landscape", type=existing_file_path, help="Optional 4:3 landscape thumbnail path") |
| 713 | tencent_upload_video_parser.add_argument("--thumbnail-portrait", type=existing_file_path, help="Optional 3:4 portrait thumbnail path") |
| 714 | tencent_upload_video_parser.add_argument("--short-title", help="Optional WeChat Channels short title") |
| 715 | tencent_upload_video_parser.add_argument("--category", help="Optional original content category") |
| 716 | tencent_upload_video_parser.add_argument("--draft", action="store_true", help="Save as draft instead of publishing") |
| 717 | add_runtime_flags(tencent_upload_video_parser) |
| 718 | |
| 719 | youtube_parser = platform_parsers.add_parser("youtube", help="YouTube operations") |
| 720 | youtube_actions = youtube_parser.add_subparsers(dest="action", required=True) |
| 721 | |
| 722 | for action_name in ("login", "check"): |
| 723 | action_parser = youtube_actions.add_parser(action_name, help=f"YouTube {action_name}") |
| 724 | action_parser.add_argument("--account", required=True, help="YouTube user-defined account_name") |
| 725 | if action_name == "login": |
| 726 | add_runtime_flags(action_parser) |
| 727 | |
| 728 | youtube_upload_video_parser = youtube_actions.add_parser("upload-video", help="Upload one video to YouTube") |
| 729 | youtube_upload_video_parser.add_argument("--account", required=True, help="YouTube user-defined account_name") |
| 730 | youtube_upload_video_parser.add_argument("--file", required=True, type=existing_file_path, help="Video file path") |
| 731 | youtube_upload_video_parser.add_argument("--title", required=True, help="Video title (<=100 chars)") |
| 732 | youtube_upload_video_parser.add_argument("--desc", default="", help="Optional video description") |
| 733 | youtube_upload_video_parser.add_argument("--tags", default="", help="Comma-separated tags, such as tag1,tag2") |
| 734 | youtube_upload_video_parser.add_argument("--thumbnail", type=existing_file_path, help="Optional thumbnail image path") |
| 735 | youtube_upload_video_parser.add_argument("--playlist", help="Optional playlist name to add the video to (for series)") |
| 736 | youtube_upload_video_parser.add_argument( |
| 737 | "--visibility", default="public", choices=["public", "unlisted", "private"], help="Video visibility") |
| 738 | add_runtime_flags(youtube_upload_video_parser) |
| 739 | return parser |
| 740 | |
| 741 | |
| 742 | async def dispatch(args: argparse.Namespace) -> int: |
| 743 | if args.platform == "douyin": |
| 744 | if args.action == "login": |
| 745 | result = await login_douyin_account(args.account, headless=args.headless) |
| 746 | if not result["success"]: |
| 747 | raise RuntimeError(result["message"]) |
| 748 | print(f"Douyin login flow completed: {result['account_file']}") |
| 749 | return 0 |
| 750 | |
| 751 | if args.action == "check": |
| 752 | is_valid = await check_douyin_account(args.account) |
| 753 | print("valid" if is_valid else "invalid") |
| 754 | return 0 if is_valid else 1 |
| 755 | |
| 756 | publish_strategy = DOUYIN_PUBLISH_STRATEGY_SCHEDULED if args.schedule else DOUYIN_PUBLISH_STRATEGY_IMMEDIATE |
| 757 | |
| 758 | if args.action == "upload-video": |
| 759 | request = DouyinVideoUploadRequest( |
| 760 | account_name=args.account, |
| 761 | video_file=args.file, |
| 762 | title=args.title, |
| 763 | description=args.desc, |
| 764 | tags=parse_tags(args.tags), |
| 765 | publish_date=args.schedule or 0, |
| 766 | thumbnail_file=args.thumbnail, |
| 767 | thumbnail_landscape_file=args.thumbnail_landscape, |
| 768 | thumbnail_portrait_file=args.thumbnail_portrait, |
| 769 | product_link=args.product_link, |
| 770 | product_title=args.product_title, |
| 771 | declaration=args.declaration, |
| 772 | publish_strategy=publish_strategy, |
| 773 | debug=args.debug, |
| 774 | headless=args.headless, |
| 775 | ) |
| 776 | await upload_video(request) |
| 777 | print(f"Douyin video upload submitted: {request.video_file}") |
| 778 | return 0 |
| 779 | |
| 780 | if args.action == "upload-note": |
| 781 | # 如果指定了 --notef,读取文件内容作为 note |
| 782 | note_content = args.note |
| 783 | if args.notef: |
| 784 | note_file = Path(args.notef) |
| 785 | if not note_file.exists(): |
| 786 | print(f"错误:文件不存在: {note_file}", file=sys.stderr) |
| 787 | return 1 |
| 788 | note_content = note_file.read_text(encoding="utf-8") |
| 789 | |
| 790 | request = DouyinNoteUploadRequest( |
| 791 | account_name=args.account, |
| 792 | image_files=parse_image_files(args.images), |
| 793 | title=args.title, |
| 794 | note=note_content, |
| 795 | tags=parse_tags(args.tags), |
| 796 | publish_date=args.schedule or 0, |
| 797 | publish_strategy=publish_strategy, |
| 798 | debug=args.debug, |
| 799 | headless=args.headless, |
| 800 | bgm=args.bgm or "", |
| 801 | ) |
| 802 | await upload_note(request) |
| 803 | print(f"Douyin note upload submitted: {len(request.image_files)} images") |
| 804 | return 0 |
| 805 | |
| 806 | raise RuntimeError(f"Unsupported Douyin action: {args.action}") |
| 807 | |
| 808 | if args.platform == "kuaishou": |
| 809 | if args.action == "login": |
| 810 | result = await login_kuaishou_account(args.account, headless=args.headless) |
| 811 | if not result["success"]: |
| 812 | raise RuntimeError(result["message"]) |
| 813 | print(f"Kuaishou login flow completed: {result['account_file']}") |
| 814 | return 0 |
| 815 | |
| 816 | if args.action == "check": |
| 817 | is_valid = await check_kuaishou_account(args.account) |
| 818 | print("valid" if is_valid else "invalid") |
| 819 | return 0 if is_valid else 1 |
| 820 | |
| 821 | publish_strategy = KUAISHOU_PUBLISH_STRATEGY_SCHEDULED if args.schedule else KUAISHOU_PUBLISH_STRATEGY_IMMEDIATE |
| 822 | |
| 823 | if args.action == "upload-video": |
| 824 | request = KuaishouVideoUploadRequest( |
| 825 | account_name=args.account, |
| 826 | video_file=args.file, |
| 827 | title=args.title, |
| 828 | description=args.desc, |
| 829 | tags=parse_tags(args.tags), |
| 830 | publish_date=args.schedule or 0, |
| 831 | thumbnail_file=args.thumbnail, |
| 832 | publish_strategy=publish_strategy, |
| 833 | debug=args.debug, |
| 834 | headless=args.headless, |
| 835 | ) |
| 836 | await upload_kuaishou_video(request) |
| 837 | print(f"Kuaishou video upload submitted: {request.video_file}") |
| 838 | return 0 |
| 839 | |
| 840 | if args.action == "upload-note": |
| 841 | request = KuaishouNoteUploadRequest( |
| 842 | account_name=args.account, |
| 843 | image_files=parse_image_files(args.images), |
| 844 | title=args.title, |
| 845 | note=args.note, |
| 846 | tags=parse_tags(args.tags), |
| 847 | publish_date=args.schedule or 0, |
| 848 | publish_strategy=publish_strategy, |
| 849 | debug=args.debug, |
| 850 | headless=args.headless, |
| 851 | ) |
| 852 | await upload_kuaishou_note(request) |
| 853 | print(f"Kuaishou note upload submitted: {len(request.image_files)} images") |
| 854 | return 0 |
| 855 | |
| 856 | raise RuntimeError(f"Unsupported Kuaishou action: {args.action}") |
| 857 | |
| 858 | if args.platform == "xiaohongshu": |
| 859 | if args.action == "login": |
| 860 | result = await login_xiaohongshu_account(args.account, headless=args.headless) |
| 861 | if not result["success"]: |
| 862 | raise RuntimeError(result["message"]) |
| 863 | print(f"Xiaohongshu login flow completed: {result['account_file']}") |
| 864 | return 0 |
| 865 | |
| 866 | if args.action == "check": |
| 867 | is_valid = await check_xiaohongshu_account(args.account) |
| 868 | print("valid" if is_valid else "invalid") |
| 869 | return 0 if is_valid else 1 |
| 870 | |
| 871 | publish_strategy = ( |
| 872 | XIAOHONGSHU_PUBLISH_STRATEGY_SCHEDULED if args.schedule else XIAOHONGSHU_PUBLISH_STRATEGY_IMMEDIATE |
| 873 | ) |
| 874 | |
| 875 | if args.action == "upload-video": |
| 876 | parsed_tags = parse_tags(args.tags) |
| 877 | if len(parsed_tags) > 10: |
| 878 | print(f"错误:小红书标签最多 10 个,当前提供了 {len(parsed_tags)} 个: {parsed_tags}", file=sys.stderr) |
| 879 | return 1 |
| 880 | request = XiaohongshuVideoUploadRequest( |
| 881 | account_name=args.account, |
| 882 | video_file=args.file, |
| 883 | title=args.title, |
| 884 | description=args.desc, |
| 885 | tags=parsed_tags, |
| 886 | publish_date=args.schedule or 0, |
| 887 | thumbnail_file=args.thumbnail, |
| 888 | publish_strategy=publish_strategy, |
| 889 | debug=args.debug, |
| 890 | headless=args.headless, |
| 891 | ) |
| 892 | await upload_xiaohongshu_video(request) |
| 893 | print(f"Xiaohongshu video upload submitted: {request.video_file}") |
| 894 | return 0 |
| 895 | |
| 896 | if args.action == "upload-note": |
| 897 | parsed_tags = parse_tags(args.tags) |
| 898 | if len(parsed_tags) > 10: |
| 899 | print(f"错误:小红书标签最多 10 个,当前提供了 {len(parsed_tags)} 个: {parsed_tags}", file=sys.stderr) |
| 900 | return 1 |
| 901 | request = XiaohongshuNoteUploadRequest( |
| 902 | account_name=args.account, |
| 903 | image_files=parse_image_files(args.images), |
| 904 | title=args.title, |
| 905 | note=args.note, |
| 906 | tags=parsed_tags, |
| 907 | publish_date=args.schedule or 0, |
| 908 | publish_strategy=publish_strategy, |
| 909 | debug=args.debug, |
| 910 | headless=args.headless, |
| 911 | ) |
| 912 | await upload_xiaohongshu_note(request) |
| 913 | print(f"Xiaohongshu note upload submitted: {len(request.image_files)} images") |
| 914 | return 0 |
| 915 | |
| 916 | raise RuntimeError(f"Unsupported Xiaohongshu action: {args.action}") |
| 917 | |
| 918 | if args.platform == "bilibili": |
| 919 | if args.action == "login": |
| 920 | result = await login_bilibili_account(args.account) |
| 921 | if not result["success"]: |
| 922 | raise RuntimeError(result["message"]) |
| 923 | print(f"Bilibili login flow completed: {result['account_file']}") |
| 924 | return 0 |
| 925 | |
| 926 | if args.action == "check": |
| 927 | is_valid = await check_bilibili_account(args.account) |
| 928 | print("valid" if is_valid else "invalid") |
| 929 | return 0 if is_valid else 1 |
| 930 | |
| 931 | if args.action == "upload-video": |
| 932 | request = BilibiliVideoUploadRequest( |
| 933 | account_name=args.account, |
| 934 | video_file=args.file, |
| 935 | title=args.title, |
| 936 | description=args.desc, |
| 937 | tid=args.tid, |
| 938 | tags=parse_tags(args.tags), |
| 939 | publish_date=args.schedule or 0, |
| 940 | thumbnail_file=args.thumbnail, |
| 941 | ) |
| 942 | await upload_bilibili_video(request) |
| 943 | print(f"Bilibili video upload submitted: {request.video_file}") |
| 944 | return 0 |
| 945 | |
| 946 | raise RuntimeError(f"Unsupported Bilibili action: {args.action}") |
| 947 | |
| 948 | if args.platform == "tencent": |
| 949 | if args.action == "login": |
| 950 | result = await login_tencent_account(args.account, headless=args.headless) |
| 951 | if not result["success"]: |
| 952 | raise RuntimeError(result["message"]) |
| 953 | print(f"Tencent/WeChat Channels login flow completed: {result['account_file']}") |
| 954 | return 0 |
| 955 | |
| 956 | if args.action == "check": |
| 957 | is_valid = await check_tencent_account(args.account) |
| 958 | print("valid" if is_valid else "invalid") |
| 959 | return 0 if is_valid else 1 |
| 960 | |
| 961 | publish_strategy = TENCENT_PUBLISH_STRATEGY_SCHEDULED if args.schedule else TENCENT_PUBLISH_STRATEGY_IMMEDIATE |
| 962 | |
| 963 | if args.action == "upload-video": |
| 964 | request = TencentVideoUploadRequest( |
| 965 | account_name=args.account, |
| 966 | video_file=args.file, |
| 967 | title=args.title, |
| 968 | description=args.desc, |
| 969 | tags=parse_tags(args.tags), |
| 970 | publish_date=args.schedule or 0, |
| 971 | thumbnail_file=args.thumbnail, |
| 972 | thumbnail_landscape_file=args.thumbnail_landscape, |
| 973 | thumbnail_portrait_file=args.thumbnail_portrait, |
| 974 | short_title=args.short_title, |
| 975 | category=args.category, |
| 976 | is_draft=args.draft, |
| 977 | publish_strategy=publish_strategy, |
| 978 | debug=args.debug, |
| 979 | headless=args.headless, |
| 980 | ) |
| 981 | await upload_tencent_video(request) |
| 982 | print(f"Tencent/WeChat Channels video upload submitted: {request.video_file}") |
| 983 | return 0 |
| 984 | |
| 985 | raise RuntimeError(f"Unsupported Tencent/WeChat Channels action: {args.action}") |
| 986 | |
| 987 | if args.platform == "youtube": |
| 988 | if args.action == "login": |
| 989 | result = await login_youtube_account(args.account, headless=args.headless) |
| 990 | if not result["success"]: |
| 991 | raise RuntimeError(result["message"]) |
| 992 | print(f"YouTube login flow completed: {result['account_file']}") |
| 993 | return 0 |
| 994 | |
| 995 | if args.action == "check": |
| 996 | is_valid = await check_youtube_account(args.account) |
| 997 | print("valid" if is_valid else "invalid") |
| 998 | return 0 if is_valid else 1 |
| 999 | |
| 1000 | if args.action == "upload-video": |
| 1001 | request = YouTubeVideoUploadRequest( |
| 1002 | account_name=args.account, |
| 1003 | video_file=args.file, |
| 1004 | title=args.title, |
| 1005 | description=args.desc, |
| 1006 | tags=parse_tags(args.tags), |
| 1007 | thumbnail_file=args.thumbnail, |
| 1008 | playlist=args.playlist, |
| 1009 | visibility=args.visibility, |
| 1010 | debug=args.debug, |
| 1011 | headless=args.headless, |
| 1012 | ) |
| 1013 | await upload_youtube_video(request) |
| 1014 | print(f"YouTube video upload submitted: {request.video_file}") |
| 1015 | return 0 |
| 1016 | |
| 1017 | raise RuntimeError(f"Unsupported YouTube action: {args.action}") |
| 1018 | |
| 1019 | raise RuntimeError(f"Unsupported platform: {args.platform}") |
| 1020 | |
| 1021 | |
| 1022 | def main(argv: Sequence[str] | None = None) -> int: |
| 1023 | parser = build_parser() |
| 1024 | args = parser.parse_args(list(argv) if argv is not None else None) |
| 1025 | try: |
| 1026 | return asyncio.run(dispatch(args)) |
| 1027 | except Exception as exc: |
| 1028 | print(str(exc), file=sys.stderr) |
| 1029 | return 1 |
| 1030 | |
| 1031 | |
| 1032 | if __name__ == "__main__": |
| 1033 | raise SystemExit(main()) |
| 1034 |