Files
TrueGrowth/vendor/social-auto-upload/sau_cli.py
Jammy 52636c91ae
Some checks failed
CI / main (push) Has been cancelled
CI / release-e2e (push) Has been cancelled
Track bundled vendor runtime sources
2026-07-07 10:05:50 +08:00

1022 lines
43 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import argparse
import asyncio
import sys
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import Iterable, Sequence
from conf import BASE_DIR
from uploader.bilibili_uploader.runtime import run_biliup_command
from uploader.douyin_uploader.main import (
DOUYIN_PUBLISH_STRATEGY_IMMEDIATE,
DOUYIN_PUBLISH_STRATEGY_SCHEDULED,
DouYinNote,
DouYinVideo,
cookie_auth as douyin_cookie_auth,
douyin_setup,
)
from uploader.ks_uploader.main import (
KUAISHOU_PUBLISH_STRATEGY_IMMEDIATE,
KUAISHOU_PUBLISH_STRATEGY_SCHEDULED,
KSNote,
KSVideo,
cookie_auth as kuaishou_cookie_auth,
ks_setup,
)
from uploader.tencent_uploader.main import (
TENCENT_PUBLISH_STRATEGY_IMMEDIATE,
TENCENT_PUBLISH_STRATEGY_SCHEDULED,
TencentVideo,
cookie_auth as tencent_cookie_auth,
tencent_setup,
)
from uploader.xiaohongshu_uploader.main import (
XIAOHONGSHU_PUBLISH_STRATEGY_IMMEDIATE,
XIAOHONGSHU_PUBLISH_STRATEGY_SCHEDULED,
XiaoHongShuNote,
XiaoHongShuVideo,
cookie_auth as xiaohongshu_cookie_auth,
xiaohongshu_setup,
)
from uploader.youtube_uploader.main import (
YouTubeVideo,
cookie_auth as youtube_cookie_auth,
youtube_setup,
)
SCHEDULE_FORMAT = "%Y-%m-%d %H:%M"
@dataclass(slots=True)
class DouyinVideoUploadRequest:
account_name: str
video_file: Path
title: str
description: str
tags: list[str]
publish_date: datetime | int
thumbnail_file: Path | None = None
thumbnail_landscape_file: Path | None = None
thumbnail_portrait_file: Path | None = None
product_link: str = ""
product_title: str = ""
publish_strategy: str = DOUYIN_PUBLISH_STRATEGY_IMMEDIATE
debug: bool = True
headless: bool = True
@dataclass(slots=True)
class DouyinNoteUploadRequest:
account_name: str
image_files: list[Path]
title: str
note: str
tags: list[str]
publish_date: datetime | int
publish_strategy: str = DOUYIN_PUBLISH_STRATEGY_IMMEDIATE
debug: bool = True
headless: bool = True
bgm: str = ""
@dataclass(slots=True)
class KuaishouVideoUploadRequest:
account_name: str
video_file: Path
title: str
description: str
tags: list[str]
publish_date: datetime | int
thumbnail_file: Path | None = None
publish_strategy: str = KUAISHOU_PUBLISH_STRATEGY_IMMEDIATE
debug: bool = True
headless: bool = True
@dataclass(slots=True)
class KuaishouNoteUploadRequest:
account_name: str
image_files: list[Path]
title: str
note: str
tags: list[str]
publish_date: datetime | int
publish_strategy: str = KUAISHOU_PUBLISH_STRATEGY_IMMEDIATE
debug: bool = True
headless: bool = True
@dataclass(slots=True)
class XiaohongshuVideoUploadRequest:
account_name: str
video_file: Path
title: str
description: str
tags: list[str]
publish_date: datetime | int
thumbnail_file: Path | None = None
publish_strategy: str = XIAOHONGSHU_PUBLISH_STRATEGY_IMMEDIATE
debug: bool = True
headless: bool = True
@dataclass(slots=True)
class XiaohongshuNoteUploadRequest:
account_name: str
image_files: list[Path]
title: str
note: str
tags: list[str]
publish_date: datetime | int
publish_strategy: str = XIAOHONGSHU_PUBLISH_STRATEGY_IMMEDIATE
debug: bool = True
headless: bool = True
@dataclass(slots=True)
class BilibiliVideoUploadRequest:
account_name: str
video_file: Path
title: str
description: str
tid: int
tags: list[str]
publish_date: datetime | int
@dataclass(slots=True)
class TencentVideoUploadRequest:
account_name: str
video_file: Path
title: str
description: str
tags: list[str]
publish_date: datetime | int
thumbnail_file: Path | None = None
thumbnail_landscape_file: Path | None = None
thumbnail_portrait_file: Path | None = None
short_title: str | None = None
category: str | None = None
is_draft: bool = False
publish_strategy: str = TENCENT_PUBLISH_STRATEGY_IMMEDIATE
debug: bool = True
headless: bool = True
@dataclass(slots=True)
class YouTubeVideoUploadRequest:
account_name: str
video_file: Path
title: str
description: str
tags: list[str]
thumbnail_file: Path | None = None
playlist: str | None = None
visibility: str = "public"
debug: bool = True
headless: bool = False
def has_interactive_terminal() -> bool:
return sys.stdin.isatty() and sys.stdout.isatty()
def resolve_runtime_home() -> Path:
return Path(BASE_DIR)
def resolve_account_file(platform: str, account_name: str) -> Path:
account_file = resolve_runtime_home() / "cookies" / f"{platform}_{account_name}.json"
account_file.parent.mkdir(exist_ok=True)
return account_file
def parse_tags(raw_tags: str | None) -> list[str]:
if not raw_tags:
return []
tags: list[str] = []
for item in raw_tags.split(","):
cleaned = item.strip().lstrip("#")
if cleaned:
tags.append(cleaned)
return tags
def parse_image_files(raw_files: Iterable[Path]) -> list[Path]:
return [Path(file) for file in raw_files]
def parse_schedule(raw_schedule: str | None) -> datetime | int:
if not raw_schedule:
return 0
return datetime.strptime(raw_schedule, SCHEDULE_FORMAT)
async def login_douyin_account(account_name: str, headless: bool = True) -> dict:
account_file = resolve_account_file("douyin", account_name)
return await douyin_setup(str(account_file), handle=True, return_detail=True, headless=headless)
async def check_douyin_account(account_name: str) -> bool:
account_file = resolve_account_file("douyin", account_name)
if not account_file.exists():
return False
return await douyin_cookie_auth(str(account_file))
async def login_kuaishou_account(account_name: str, headless: bool = True) -> dict:
account_file = resolve_account_file("kuaishou", account_name)
return await ks_setup(str(account_file), handle=True, return_detail=True, headless=headless)
async def check_kuaishou_account(account_name: str) -> bool:
account_file = resolve_account_file("kuaishou", account_name)
if not account_file.exists():
return False
return await kuaishou_cookie_auth(str(account_file))
async def login_xiaohongshu_account(account_name: str, headless: bool = True) -> dict:
account_file = resolve_account_file("xiaohongshu", account_name)
return await xiaohongshu_setup(str(account_file), handle=True, return_detail=True, headless=headless)
async def check_xiaohongshu_account(account_name: str) -> bool:
account_file = resolve_account_file("xiaohongshu", account_name)
if not account_file.exists():
return False
return await xiaohongshu_cookie_auth(str(account_file))
async def login_bilibili_account(account_name: str) -> dict:
account_file = resolve_account_file("bilibili", account_name)
if not has_interactive_terminal():
return {
"success": False,
"message": (
"Bilibili login requires a local interactive terminal. "
f"Please run `sau bilibili login --account {account_name}` yourself in a local terminal. "
"If the terminal QR code does not render completely, open `./qrcode.png` and scan that image."
),
"account_file": str(account_file),
}
result = run_biliup_command(["-u", str(account_file), "login"], interactive=True)
success = result.returncode == 0
return {
"success": success,
"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",
"account_file": str(account_file),
}
async def check_bilibili_account(account_name: str) -> bool:
account_file = resolve_account_file("bilibili", account_name)
if not account_file.exists():
return False
result = run_biliup_command(["-u", str(account_file), "renew"])
return result.returncode == 0
async def login_tencent_account(account_name: str, headless: bool = True) -> dict:
account_file = resolve_account_file("tencent", account_name)
return await tencent_setup(str(account_file), handle=True, return_detail=True, headless=headless)
async def check_tencent_account(account_name: str) -> bool:
account_file = resolve_account_file("tencent", account_name)
if not account_file.exists():
return False
return await tencent_cookie_auth(str(account_file))
async def login_youtube_account(account_name: str, headless: bool = False) -> dict:
account_file = resolve_account_file("youtube", account_name)
return await youtube_setup(str(account_file), handle=True, return_detail=True, headless=headless)
async def check_youtube_account(account_name: str) -> bool:
account_file = resolve_account_file("youtube", account_name)
if not account_file.exists():
return False
return await youtube_cookie_auth(str(account_file))
async def upload_youtube_video(request: YouTubeVideoUploadRequest) -> Path:
account_file = resolve_account_file("youtube", request.account_name)
is_ready = await youtube_setup(str(account_file), handle=False)
if not is_ready:
raise RuntimeError(
f"YouTube cookie is missing or expired: {account_file}. Run `sau youtube login --account {request.account_name}` first."
)
app = YouTubeVideo(
request.title,
str(request.video_file),
request.tags,
str(account_file),
description=request.description,
thumbnail_path=str(request.thumbnail_file) if request.thumbnail_file else None,
playlist=request.playlist,
visibility=request.visibility,
debug=request.debug,
headless=request.headless,
)
await app.main()
return account_file
async def upload_video(request: DouyinVideoUploadRequest) -> Path:
account_file = resolve_account_file("douyin", request.account_name)
is_ready = await douyin_setup(str(account_file), handle=False)
if not is_ready:
raise RuntimeError(
f"Douyin cookie is missing or expired: {account_file}. Run `sau douyin login --account {request.account_name}` first."
)
app = DouYinVideo(
request.title,
str(request.video_file),
request.tags,
request.publish_date,
str(account_file),
desc=request.description,
thumbnail_landscape_path=(
str(request.thumbnail_landscape_file) if request.thumbnail_landscape_file else None
),
thumbnail_portrait_path=str(
request.thumbnail_portrait_file or request.thumbnail_file
) if request.thumbnail_portrait_file or request.thumbnail_file else None,
productLink=request.product_link,
productTitle=request.product_title,
publish_strategy=request.publish_strategy,
debug=request.debug,
headless=request.headless,
)
await app.douyin_upload_video()
return account_file
async def upload_note(request: DouyinNoteUploadRequest) -> Path:
account_file = resolve_account_file("douyin", request.account_name)
is_ready = await douyin_setup(str(account_file), handle=False)
if not is_ready:
raise RuntimeError(
f"Douyin cookie is missing or expired: {account_file}. Run `sau douyin login --account {request.account_name}` first."
)
app = DouYinNote(
image_paths=[str(path) for path in request.image_files],
title=request.title,
note=request.note,
tags=request.tags,
publish_date=request.publish_date,
account_file=str(account_file),
publish_strategy=request.publish_strategy,
debug=request.debug,
headless=request.headless,
bgm=request.bgm,
)
await app.douyin_upload_note()
return account_file
async def upload_kuaishou_video(request: KuaishouVideoUploadRequest) -> Path:
account_file = resolve_account_file("kuaishou", request.account_name)
is_ready = await ks_setup(str(account_file), handle=False)
if not is_ready:
raise RuntimeError(
f"Kuaishou cookie is missing or expired: {account_file}. Run `sau kuaishou login --account {request.account_name}` first."
)
app = KSVideo(
title=request.title,
file_path=str(request.video_file),
desc=request.description,
tags=request.tags,
publish_date=request.publish_date,
account_file=str(account_file),
thumbnail_path=str(request.thumbnail_file) if request.thumbnail_file else None,
publish_strategy=request.publish_strategy,
debug=request.debug,
headless=request.headless,
)
await app.main()
return account_file
async def upload_kuaishou_note(request: KuaishouNoteUploadRequest) -> Path:
account_file = resolve_account_file("kuaishou", request.account_name)
is_ready = await ks_setup(str(account_file), handle=False)
if not is_ready:
raise RuntimeError(
f"Kuaishou cookie is missing or expired: {account_file}. Run `sau kuaishou login --account {request.account_name}` first."
)
app = KSNote(
image_paths=[str(path) for path in request.image_files],
title=request.title,
note=request.note,
tags=request.tags,
publish_date=request.publish_date,
account_file=str(account_file),
publish_strategy=request.publish_strategy,
debug=request.debug,
headless=request.headless,
)
await app.main()
return account_file
async def upload_xiaohongshu_video(request: XiaohongshuVideoUploadRequest) -> Path:
account_file = resolve_account_file("xiaohongshu", request.account_name)
is_ready = await xiaohongshu_setup(str(account_file), handle=False)
if not is_ready:
raise RuntimeError(
f"Xiaohongshu cookie is missing or expired: {account_file}. Run `sau xiaohongshu login --account {request.account_name}` first."
)
app = XiaoHongShuVideo(
title=request.title,
file_path=str(request.video_file),
desc=request.description,
tags=request.tags,
publish_date=request.publish_date,
account_file=str(account_file),
thumbnail_path=str(request.thumbnail_file) if request.thumbnail_file else None,
publish_strategy=request.publish_strategy,
debug=request.debug,
headless=request.headless,
)
await app.main()
return account_file
async def upload_xiaohongshu_note(request: XiaohongshuNoteUploadRequest) -> Path:
account_file = resolve_account_file("xiaohongshu", request.account_name)
is_ready = await xiaohongshu_setup(str(account_file), handle=False)
if not is_ready:
raise RuntimeError(
f"Xiaohongshu cookie is missing or expired: {account_file}. Run `sau xiaohongshu login --account {request.account_name}` first."
)
app = XiaoHongShuNote(
image_paths=[str(path) for path in request.image_files],
title=request.title,
desc=request.note,
note=request.note,
tags=request.tags,
publish_date=request.publish_date,
account_file=str(account_file),
publish_strategy=request.publish_strategy,
debug=request.debug,
headless=request.headless,
)
await app.main()
return account_file
async def upload_bilibili_video(request: BilibiliVideoUploadRequest) -> Path:
account_file = resolve_account_file("bilibili", request.account_name)
if not account_file.exists():
raise RuntimeError(
f"Bilibili account file is missing: {account_file}. Run `sau bilibili login --account {request.account_name}` first."
)
arguments = [
"-u",
str(account_file),
"upload",
str(request.video_file),
"--title",
request.title,
"--desc",
request.description,
"--tid",
str(request.tid),
]
if request.tags:
arguments.extend(["--tag", ",".join(request.tags)])
if isinstance(request.publish_date, datetime):
arguments.extend(["--dtime", str(int(request.publish_date.timestamp()))])
result = run_biliup_command(arguments)
if result.returncode != 0:
raise RuntimeError((result.stderr or result.stdout or "").strip() or "Bilibili upload failed")
return account_file
async def upload_tencent_video(request: TencentVideoUploadRequest) -> Path:
account_file = resolve_account_file("tencent", request.account_name)
is_ready = await tencent_setup(str(account_file), handle=False)
if not is_ready:
raise RuntimeError(
f"Tencent/WeChat Channels cookie is missing or expired: {account_file}. "
f"Run `sau tencent login --account {request.account_name}` first."
)
app = TencentVideo(
title=request.title,
file_path=str(request.video_file),
tags=request.tags,
publish_date=request.publish_date,
account_file=str(account_file),
category=request.category,
is_draft=request.is_draft,
desc=request.description,
thumbnail_path=str(request.thumbnail_file) if request.thumbnail_file else None,
thumbnail_landscape_path=(
str(request.thumbnail_landscape_file) if request.thumbnail_landscape_file else None
),
thumbnail_portrait_path=(
str(request.thumbnail_portrait_file) if request.thumbnail_portrait_file else None
),
short_title=request.short_title,
publish_strategy=request.publish_strategy,
debug=request.debug,
headless=request.headless,
)
await app.tencent_upload_video()
return account_file
def existing_file_path(value: str) -> Path:
path = Path(value)
if not path.is_file():
raise argparse.ArgumentTypeError(f"File not found: {value}")
return path
def schedule_value(value: str):
try:
return parse_schedule(value)
except ValueError as exc:
raise argparse.ArgumentTypeError(
f"Invalid schedule '{value}'. Expected format: {SCHEDULE_FORMAT}"
) from exc
def add_runtime_flags(parser: argparse.ArgumentParser) -> None:
parser.add_argument("--debug", action="store_true", help="Enable debug mode")
headless_group = parser.add_mutually_exclusive_group()
headless_group.add_argument("--headed", dest="headless", action="store_false", help="Run with browser UI")
headless_group.add_argument("--headless", dest="headless", action="store_true", help="Run in headless mode")
parser.set_defaults(headless=True)
def build_parser() -> argparse.ArgumentParser:
schedule_help = SCHEDULE_FORMAT.replace("%", "%%")
parser = argparse.ArgumentParser(
prog="sau",
description="CLI for social-auto-upload.",
)
platform_parsers = parser.add_subparsers(dest="platform", required=True)
douyin_parser = platform_parsers.add_parser("douyin", help="Douyin operations")
douyin_actions = douyin_parser.add_subparsers(dest="action", required=True)
for action_name in ("login", "check"):
action_parser = douyin_actions.add_parser(action_name, help=f"Douyin {action_name}")
action_parser.add_argument("--account", required=True, help="Douyin user-defined account_name")
if action_name == "login":
add_runtime_flags(action_parser)
upload_video_parser = douyin_actions.add_parser("upload-video", help="Upload one video to Douyin")
upload_video_parser.add_argument("--account", required=True, help="Douyin user-defined account_name")
upload_video_parser.add_argument("--file", required=True, type=existing_file_path, help="Video file path")
upload_video_parser.add_argument("--title", required=True, help="Video title")
upload_video_parser.add_argument("--desc", default="", help="Optional video description")
upload_video_parser.add_argument("--tags", default="", help="Comma-separated tags, such as tag1,tag2")
upload_video_parser.add_argument("--schedule", type=schedule_value, help=f"Schedule time in {schedule_help}")
upload_video_parser.add_argument("--thumbnail", type=existing_file_path, help="Optional 3:4 portrait thumbnail path")
upload_video_parser.add_argument("--thumbnail-landscape", type=existing_file_path, help="Optional 4:3 landscape thumbnail path")
upload_video_parser.add_argument("--thumbnail-portrait", type=existing_file_path, help="Optional 3:4 portrait thumbnail path")
upload_video_parser.add_argument("--product-link", default="", help="Optional product link")
upload_video_parser.add_argument("--product-title", default="", help="Optional product title")
add_runtime_flags(upload_video_parser)
upload_note_parser = douyin_actions.add_parser("upload-note", help="Upload one note to Douyin")
upload_note_parser.add_argument("--account", required=True, help="Douyin user-defined account_name")
upload_note_parser.add_argument("--images", required=True, nargs="+", type=existing_file_path, help="Image file paths")
upload_note_parser.add_argument("--title", required=True, help="Note title")
upload_note_parser.add_argument("--note", default="", help="Optional note content")
upload_note_parser.add_argument("--notef", default="", help="Read note content from file (txt/md)")
upload_note_parser.add_argument("--tags", default="", help="Comma-separated tags, such as tag1,tag2")
upload_note_parser.add_argument("--bgm", default="", help="BGM music name to search and select")
upload_note_parser.add_argument("--schedule", type=schedule_value, help=f"Schedule time in {schedule_help}")
add_runtime_flags(upload_note_parser)
kuaishou_parser = platform_parsers.add_parser("kuaishou", help="Kuaishou operations")
kuaishou_actions = kuaishou_parser.add_subparsers(dest="action", required=True)
for action_name in ("login", "check"):
action_parser = kuaishou_actions.add_parser(action_name, help=f"Kuaishou {action_name}")
action_parser.add_argument("--account", required=True, help="Kuaishou user-defined account_name")
if action_name == "login":
add_runtime_flags(action_parser)
kuaishou_upload_video_parser = kuaishou_actions.add_parser("upload-video", help="Upload one video to Kuaishou")
kuaishou_upload_video_parser.add_argument("--account", required=True, help="Kuaishou user-defined account_name")
kuaishou_upload_video_parser.add_argument("--file", required=True, type=existing_file_path, help="Video file path")
kuaishou_upload_video_parser.add_argument("--title", required=True, help="Video title")
kuaishou_upload_video_parser.add_argument("--desc", default="", help="Optional video description")
kuaishou_upload_video_parser.add_argument("--tags", default="", help="Comma-separated tags, such as tag1,tag2")
kuaishou_upload_video_parser.add_argument("--schedule", type=schedule_value, help=f"Schedule time in {schedule_help}")
kuaishou_upload_video_parser.add_argument("--thumbnail", type=existing_file_path, help="Optional thumbnail path")
add_runtime_flags(kuaishou_upload_video_parser)
kuaishou_upload_note_parser = kuaishou_actions.add_parser("upload-note", help="Upload one note to Kuaishou")
kuaishou_upload_note_parser.add_argument("--account", required=True, help="Kuaishou user-defined account_name")
kuaishou_upload_note_parser.add_argument("--images", required=True, nargs="+", type=existing_file_path, help="Image file paths")
kuaishou_upload_note_parser.add_argument("--title", required=True, help="Note title")
kuaishou_upload_note_parser.add_argument("--note", default="", help="Optional note content")
kuaishou_upload_note_parser.add_argument("--tags", default="", help="Comma-separated tags, such as tag1,tag2")
kuaishou_upload_note_parser.add_argument("--schedule", type=schedule_value, help=f"Schedule time in {schedule_help}")
add_runtime_flags(kuaishou_upload_note_parser)
xiaohongshu_parser = platform_parsers.add_parser("xiaohongshu", help="Xiaohongshu operations")
xiaohongshu_actions = xiaohongshu_parser.add_subparsers(dest="action", required=True)
for action_name in ("login", "check"):
action_parser = xiaohongshu_actions.add_parser(action_name, help=f"Xiaohongshu {action_name}")
action_parser.add_argument("--account", required=True, help="Xiaohongshu user-defined account_name")
if action_name == "login":
add_runtime_flags(action_parser)
xiaohongshu_upload_video_parser = xiaohongshu_actions.add_parser("upload-video", help="Upload one video to Xiaohongshu")
xiaohongshu_upload_video_parser.add_argument("--account", required=True, help="Xiaohongshu user-defined account_name")
xiaohongshu_upload_video_parser.add_argument("--file", required=True, type=existing_file_path, help="Video file path")
xiaohongshu_upload_video_parser.add_argument("--title", required=True, help="Video title")
xiaohongshu_upload_video_parser.add_argument("--desc", default="", help="Optional video description")
xiaohongshu_upload_video_parser.add_argument("--tags", default="", help="Comma-separated tags, such as tag1,tag2")
xiaohongshu_upload_video_parser.add_argument("--schedule", type=schedule_value, help=f"Schedule time in {schedule_help}")
xiaohongshu_upload_video_parser.add_argument("--thumbnail", type=existing_file_path, help="Optional thumbnail path")
add_runtime_flags(xiaohongshu_upload_video_parser)
xiaohongshu_upload_note_parser = xiaohongshu_actions.add_parser("upload-note", help="Upload one note to Xiaohongshu")
xiaohongshu_upload_note_parser.add_argument("--account", required=True, help="Xiaohongshu user-defined account_name")
xiaohongshu_upload_note_parser.add_argument("--images", required=True, nargs="+", type=existing_file_path, help="Image file paths")
xiaohongshu_upload_note_parser.add_argument("--title", required=True, help="Note title")
xiaohongshu_upload_note_parser.add_argument("--note", default="", help="Optional note content")
xiaohongshu_upload_note_parser.add_argument("--tags", default="", help="Comma-separated tags, such as tag1,tag2")
xiaohongshu_upload_note_parser.add_argument("--schedule", type=schedule_value, help=f"Schedule time in {schedule_help}")
add_runtime_flags(xiaohongshu_upload_note_parser)
bilibili_parser = platform_parsers.add_parser("bilibili", help="Bilibili operations")
bilibili_actions = bilibili_parser.add_subparsers(dest="action", required=True)
for action_name in ("login", "check"):
action_parser = bilibili_actions.add_parser(action_name, help=f"Bilibili {action_name}")
action_parser.add_argument("--account", required=True, help="Bilibili user-defined account_name")
bilibili_upload_video_parser = bilibili_actions.add_parser("upload-video", help="Upload one video to Bilibili")
bilibili_upload_video_parser.add_argument("--account", required=True, help="Bilibili user-defined account_name")
bilibili_upload_video_parser.add_argument("--file", required=True, type=existing_file_path, help="Video file path")
bilibili_upload_video_parser.add_argument("--title", required=True, help="Video title")
bilibili_upload_video_parser.add_argument("--desc", required=True, help="Video description")
bilibili_upload_video_parser.add_argument("--tid", required=True, type=int, help="Bilibili category id")
bilibili_upload_video_parser.add_argument("--tags", default="", help="Comma-separated tags, such as tag1,tag2")
bilibili_upload_video_parser.add_argument("--schedule", type=schedule_value, help=f"Schedule time in {schedule_help}")
tencent_parser = platform_parsers.add_parser("tencent", help="Tencent/WeChat Channels operations")
tencent_actions = tencent_parser.add_subparsers(dest="action", required=True)
for action_name in ("login", "check"):
action_parser = tencent_actions.add_parser(action_name, help=f"Tencent/WeChat Channels {action_name}")
action_parser.add_argument("--account", required=True, help="Tencent user-defined account_name")
if action_name == "login":
add_runtime_flags(action_parser)
tencent_upload_video_parser = tencent_actions.add_parser("upload-video", help="Upload one video to WeChat Channels")
tencent_upload_video_parser.add_argument("--account", required=True, help="Tencent user-defined account_name")
tencent_upload_video_parser.add_argument("--file", required=True, type=existing_file_path, help="Video file path")
tencent_upload_video_parser.add_argument("--title", required=True, help="Video title")
tencent_upload_video_parser.add_argument("--desc", default="", help="Optional video description")
tencent_upload_video_parser.add_argument("--tags", default="", help="Comma-separated tags, such as tag1,tag2")
tencent_upload_video_parser.add_argument("--schedule", type=schedule_value, help=f"Schedule time in {schedule_help}")
tencent_upload_video_parser.add_argument("--thumbnail", type=existing_file_path, help="Optional 3:4 portrait thumbnail path")
tencent_upload_video_parser.add_argument("--thumbnail-landscape", type=existing_file_path, help="Optional 4:3 landscape thumbnail path")
tencent_upload_video_parser.add_argument("--thumbnail-portrait", type=existing_file_path, help="Optional 3:4 portrait thumbnail path")
tencent_upload_video_parser.add_argument("--short-title", help="Optional WeChat Channels short title")
tencent_upload_video_parser.add_argument("--category", help="Optional original content category")
tencent_upload_video_parser.add_argument("--draft", action="store_true", help="Save as draft instead of publishing")
add_runtime_flags(tencent_upload_video_parser)
youtube_parser = platform_parsers.add_parser("youtube", help="YouTube operations")
youtube_actions = youtube_parser.add_subparsers(dest="action", required=True)
for action_name in ("login", "check"):
action_parser = youtube_actions.add_parser(action_name, help=f"YouTube {action_name}")
action_parser.add_argument("--account", required=True, help="YouTube user-defined account_name")
if action_name == "login":
add_runtime_flags(action_parser)
youtube_upload_video_parser = youtube_actions.add_parser("upload-video", help="Upload one video to YouTube")
youtube_upload_video_parser.add_argument("--account", required=True, help="YouTube user-defined account_name")
youtube_upload_video_parser.add_argument("--file", required=True, type=existing_file_path, help="Video file path")
youtube_upload_video_parser.add_argument("--title", required=True, help="Video title (<=100 chars)")
youtube_upload_video_parser.add_argument("--desc", default="", help="Optional video description")
youtube_upload_video_parser.add_argument("--tags", default="", help="Comma-separated tags, such as tag1,tag2")
youtube_upload_video_parser.add_argument("--thumbnail", type=existing_file_path, help="Optional thumbnail image path")
youtube_upload_video_parser.add_argument("--playlist", help="Optional playlist name to add the video to (for series)")
youtube_upload_video_parser.add_argument(
"--visibility", default="public", choices=["public", "unlisted", "private"], help="Video visibility")
add_runtime_flags(youtube_upload_video_parser)
return parser
async def dispatch(args: argparse.Namespace) -> int:
if args.platform == "douyin":
if args.action == "login":
result = await login_douyin_account(args.account, headless=args.headless)
if not result["success"]:
raise RuntimeError(result["message"])
print(f"Douyin login flow completed: {result['account_file']}")
return 0
if args.action == "check":
is_valid = await check_douyin_account(args.account)
print("valid" if is_valid else "invalid")
return 0 if is_valid else 1
publish_strategy = DOUYIN_PUBLISH_STRATEGY_SCHEDULED if args.schedule else DOUYIN_PUBLISH_STRATEGY_IMMEDIATE
if args.action == "upload-video":
request = DouyinVideoUploadRequest(
account_name=args.account,
video_file=args.file,
title=args.title,
description=args.desc,
tags=parse_tags(args.tags),
publish_date=args.schedule or 0,
thumbnail_file=args.thumbnail,
thumbnail_landscape_file=args.thumbnail_landscape,
thumbnail_portrait_file=args.thumbnail_portrait,
product_link=args.product_link,
product_title=args.product_title,
publish_strategy=publish_strategy,
debug=args.debug,
headless=args.headless,
)
await upload_video(request)
print(f"Douyin video upload submitted: {request.video_file}")
return 0
if args.action == "upload-note":
# 如果指定了 --notef读取文件内容作为 note
note_content = args.note
if args.notef:
note_file = Path(args.notef)
if not note_file.exists():
print(f"错误:文件不存在: {note_file}", file=sys.stderr)
return 1
note_content = note_file.read_text(encoding="utf-8")
request = DouyinNoteUploadRequest(
account_name=args.account,
image_files=parse_image_files(args.images),
title=args.title,
note=note_content,
tags=parse_tags(args.tags),
publish_date=args.schedule or 0,
publish_strategy=publish_strategy,
debug=args.debug,
headless=args.headless,
bgm=args.bgm or "",
)
await upload_note(request)
print(f"Douyin note upload submitted: {len(request.image_files)} images")
return 0
raise RuntimeError(f"Unsupported Douyin action: {args.action}")
if args.platform == "kuaishou":
if args.action == "login":
result = await login_kuaishou_account(args.account, headless=args.headless)
if not result["success"]:
raise RuntimeError(result["message"])
print(f"Kuaishou login flow completed: {result['account_file']}")
return 0
if args.action == "check":
is_valid = await check_kuaishou_account(args.account)
print("valid" if is_valid else "invalid")
return 0 if is_valid else 1
publish_strategy = KUAISHOU_PUBLISH_STRATEGY_SCHEDULED if args.schedule else KUAISHOU_PUBLISH_STRATEGY_IMMEDIATE
if args.action == "upload-video":
request = KuaishouVideoUploadRequest(
account_name=args.account,
video_file=args.file,
title=args.title,
description=args.desc,
tags=parse_tags(args.tags),
publish_date=args.schedule or 0,
thumbnail_file=args.thumbnail,
publish_strategy=publish_strategy,
debug=args.debug,
headless=args.headless,
)
await upload_kuaishou_video(request)
print(f"Kuaishou video upload submitted: {request.video_file}")
return 0
if args.action == "upload-note":
request = KuaishouNoteUploadRequest(
account_name=args.account,
image_files=parse_image_files(args.images),
title=args.title,
note=args.note,
tags=parse_tags(args.tags),
publish_date=args.schedule or 0,
publish_strategy=publish_strategy,
debug=args.debug,
headless=args.headless,
)
await upload_kuaishou_note(request)
print(f"Kuaishou note upload submitted: {len(request.image_files)} images")
return 0
raise RuntimeError(f"Unsupported Kuaishou action: {args.action}")
if args.platform == "xiaohongshu":
if args.action == "login":
result = await login_xiaohongshu_account(args.account, headless=args.headless)
if not result["success"]:
raise RuntimeError(result["message"])
print(f"Xiaohongshu login flow completed: {result['account_file']}")
return 0
if args.action == "check":
is_valid = await check_xiaohongshu_account(args.account)
print("valid" if is_valid else "invalid")
return 0 if is_valid else 1
publish_strategy = (
XIAOHONGSHU_PUBLISH_STRATEGY_SCHEDULED if args.schedule else XIAOHONGSHU_PUBLISH_STRATEGY_IMMEDIATE
)
if args.action == "upload-video":
parsed_tags = parse_tags(args.tags)
if len(parsed_tags) > 10:
print(f"错误:小红书标签最多 10 个,当前提供了 {len(parsed_tags)} 个: {parsed_tags}", file=sys.stderr)
return 1
request = XiaohongshuVideoUploadRequest(
account_name=args.account,
video_file=args.file,
title=args.title,
description=args.desc,
tags=parsed_tags,
publish_date=args.schedule or 0,
thumbnail_file=args.thumbnail,
publish_strategy=publish_strategy,
debug=args.debug,
headless=args.headless,
)
await upload_xiaohongshu_video(request)
print(f"Xiaohongshu video upload submitted: {request.video_file}")
return 0
if args.action == "upload-note":
parsed_tags = parse_tags(args.tags)
if len(parsed_tags) > 10:
print(f"错误:小红书标签最多 10 个,当前提供了 {len(parsed_tags)} 个: {parsed_tags}", file=sys.stderr)
return 1
request = XiaohongshuNoteUploadRequest(
account_name=args.account,
image_files=parse_image_files(args.images),
title=args.title,
note=args.note,
tags=parsed_tags,
publish_date=args.schedule or 0,
publish_strategy=publish_strategy,
debug=args.debug,
headless=args.headless,
)
await upload_xiaohongshu_note(request)
print(f"Xiaohongshu note upload submitted: {len(request.image_files)} images")
return 0
raise RuntimeError(f"Unsupported Xiaohongshu action: {args.action}")
if args.platform == "bilibili":
if args.action == "login":
result = await login_bilibili_account(args.account)
if not result["success"]:
raise RuntimeError(result["message"])
print(f"Bilibili login flow completed: {result['account_file']}")
return 0
if args.action == "check":
is_valid = await check_bilibili_account(args.account)
print("valid" if is_valid else "invalid")
return 0 if is_valid else 1
if args.action == "upload-video":
request = BilibiliVideoUploadRequest(
account_name=args.account,
video_file=args.file,
title=args.title,
description=args.desc,
tid=args.tid,
tags=parse_tags(args.tags),
publish_date=args.schedule or 0,
)
await upload_bilibili_video(request)
print(f"Bilibili video upload submitted: {request.video_file}")
return 0
raise RuntimeError(f"Unsupported Bilibili action: {args.action}")
if args.platform == "tencent":
if args.action == "login":
result = await login_tencent_account(args.account, headless=args.headless)
if not result["success"]:
raise RuntimeError(result["message"])
print(f"Tencent/WeChat Channels login flow completed: {result['account_file']}")
return 0
if args.action == "check":
is_valid = await check_tencent_account(args.account)
print("valid" if is_valid else "invalid")
return 0 if is_valid else 1
publish_strategy = TENCENT_PUBLISH_STRATEGY_SCHEDULED if args.schedule else TENCENT_PUBLISH_STRATEGY_IMMEDIATE
if args.action == "upload-video":
request = TencentVideoUploadRequest(
account_name=args.account,
video_file=args.file,
title=args.title,
description=args.desc,
tags=parse_tags(args.tags),
publish_date=args.schedule or 0,
thumbnail_file=args.thumbnail,
thumbnail_landscape_file=args.thumbnail_landscape,
thumbnail_portrait_file=args.thumbnail_portrait,
short_title=args.short_title,
category=args.category,
is_draft=args.draft,
publish_strategy=publish_strategy,
debug=args.debug,
headless=args.headless,
)
await upload_tencent_video(request)
print(f"Tencent/WeChat Channels video upload submitted: {request.video_file}")
return 0
raise RuntimeError(f"Unsupported Tencent/WeChat Channels action: {args.action}")
if args.platform == "youtube":
if args.action == "login":
result = await login_youtube_account(args.account, headless=args.headless)
if not result["success"]:
raise RuntimeError(result["message"])
print(f"YouTube login flow completed: {result['account_file']}")
return 0
if args.action == "check":
is_valid = await check_youtube_account(args.account)
print("valid" if is_valid else "invalid")
return 0 if is_valid else 1
if args.action == "upload-video":
request = YouTubeVideoUploadRequest(
account_name=args.account,
video_file=args.file,
title=args.title,
description=args.desc,
tags=parse_tags(args.tags),
thumbnail_file=args.thumbnail,
playlist=args.playlist,
visibility=args.visibility,
debug=args.debug,
headless=args.headless,
)
await upload_youtube_video(request)
print(f"YouTube video upload submitted: {request.video_file}")
return 0
raise RuntimeError(f"Unsupported YouTube action: {args.action}")
raise RuntimeError(f"Unsupported platform: {args.platform}")
def main(argv: Sequence[str] | None = None) -> int:
parser = build_parser()
args = parser.parse_args(list(argv) if argv is not None else None)
try:
return asyncio.run(dispatch(args))
except Exception as exc:
print(str(exc), file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main())