dy-cli 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,200 @@
1
+ """
2
+ dy init — 新用户引导式初始化。
3
+
4
+ 自动完成: 检查环境 → 安装 Chromium → 配置代理 → 登录。
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import os
9
+ import sys
10
+ import shutil
11
+ import subprocess
12
+
13
+ import click
14
+ from rich.panel import Panel
15
+
16
+ from dy_cli.utils import config
17
+ from dy_cli.utils.output import success, error, info, warning, console, status
18
+
19
+
20
+ @click.command("init", help="🚀 初始化设置 (新用户从这里开始)")
21
+ @click.option("--proxy", default=None, help="代理地址 (如 http://127.0.0.1:7897)")
22
+ @click.option("--no-proxy", is_flag=True, help="不使用代理 (在国内网络)")
23
+ @click.option("--skip-login", is_flag=True, help="跳过登录步骤")
24
+ @click.option("--skip-chromium", is_flag=True, help="跳过 Chromium 安装")
25
+ def init(proxy, no_proxy, skip_login, skip_chromium):
26
+ """引导新用户完成初始化。"""
27
+ console.print()
28
+ console.print(Panel(
29
+ "[bold]欢迎使用 🎬 dy-cli — 抖音命令行工具[/]\n\n"
30
+ "接下来将引导你完成初始化设置:\n"
31
+ " [dim]1.[/] 检查系统环境\n"
32
+ " [dim]2.[/] 安装 Playwright Chromium\n"
33
+ " [dim]3.[/] 配置网络\n"
34
+ " [dim]4.[/] 登录抖音账号",
35
+ title="🚀 初始化向导",
36
+ border_style="blue",
37
+ ))
38
+ console.print()
39
+
40
+ # ── Step 1: 环境检查 ──────────────────────────────────
41
+ console.rule("[bold]Step 1/4 — 环境检查[/]")
42
+ console.print()
43
+
44
+ status("系统", f"{sys.platform} {os.uname().machine}")
45
+ status("Python", f"{sys.version.split()[0]}")
46
+
47
+ # Check playwright
48
+ pw_ok = _check_playwright()
49
+ if pw_ok:
50
+ status("Playwright", "✅ 已安装", "green")
51
+ else:
52
+ status("Playwright", "⚠️ 未安装", "yellow")
53
+
54
+ # Check httpx
55
+ try:
56
+ import httpx
57
+ status("httpx", f"✅ {httpx.__version__}", "green")
58
+ except ImportError:
59
+ status("httpx", "⚠️ 未安装", "yellow")
60
+
61
+ console.print()
62
+
63
+ # ── Step 2: 安装 Chromium ─────────────────────────────
64
+ console.rule("[bold]Step 2/4 — 安装 Playwright Chromium[/]")
65
+ console.print()
66
+
67
+ if skip_chromium:
68
+ info("已跳过 Chromium 安装")
69
+ elif pw_ok and _check_chromium():
70
+ success("Chromium 已安装")
71
+ else:
72
+ info("正在安装 Playwright Chromium (首次运行需要)...")
73
+ try:
74
+ subprocess.run(
75
+ [sys.executable, "-m", "playwright", "install", "chromium"],
76
+ check=True,
77
+ capture_output=True,
78
+ )
79
+ success("Chromium 安装完成")
80
+ except subprocess.CalledProcessError as e:
81
+ warning(f"Chromium 安装失败: {e.stderr.decode()[:200]}")
82
+ info("请手动运行: playwright install chromium")
83
+ except FileNotFoundError:
84
+ warning("playwright 未安装,请先运行: pip install playwright")
85
+
86
+ console.print()
87
+
88
+ # ── Step 3: 网络配置 ──────────────────────────────────
89
+ console.rule("[bold]Step 3/4 — 网络配置[/]")
90
+ console.print()
91
+
92
+ cfg = config.load_config()
93
+
94
+ if no_proxy:
95
+ proxy_addr = ""
96
+ info("不使用代理 (国内网络直连)")
97
+ elif proxy:
98
+ proxy_addr = proxy
99
+ info(f"使用指定代理: {proxy}")
100
+ else:
101
+ console.print(" 抖音在国内可以直连,海外可能需要代理。")
102
+ console.print(" 如果在[bold]国内[/],直接回车跳过。")
103
+ console.print()
104
+ proxy_addr = click.prompt(
105
+ " 代理地址",
106
+ default=cfg["api"].get("proxy", ""),
107
+ show_default=True,
108
+ )
109
+ if proxy_addr.strip().lower() in ("none", "no", "skip", "跳过", "无", ""):
110
+ proxy_addr = ""
111
+
112
+ cfg["api"]["proxy"] = proxy_addr
113
+ config.save_config(cfg)
114
+ success("配置已保存")
115
+ console.print()
116
+
117
+ # ── Step 4: 登录 ─────────────────────────────────────
118
+ console.rule("[bold]Step 4/4 — 登录抖音[/]")
119
+ console.print()
120
+
121
+ if skip_login:
122
+ info("已跳过登录步骤")
123
+ info("稍后使用 [bold]dy login[/] 登录")
124
+ else:
125
+ from dy_cli.engines.playwright_client import PlaywrightClient, PlaywrightError
126
+
127
+ client = PlaywrightClient(headless=False)
128
+
129
+ # Check if already logged in
130
+ if client.cookie_exists() and client.check_login():
131
+ success("已登录抖音 ✅")
132
+ else:
133
+ info("即将打开浏览器,请使用抖音 App 扫码登录...")
134
+ console.print()
135
+ console.print(Panel(
136
+ "[bold]请使用抖音 App 扫码登录:[/]\n\n"
137
+ " 1. 打开抖音 App\n"
138
+ " 2. 点击右下角 [bold]我[/]\n"
139
+ " 3. 点击右上角 [bold]☰[/] → [bold]扫一扫[/]\n"
140
+ " 4. 扫描浏览器中的二维码\n\n"
141
+ "[dim]扫码后登录会自动完成,cookies 会持久保存。[/]",
142
+ title="📱 扫码登录",
143
+ border_style="green",
144
+ ))
145
+ console.print()
146
+
147
+ try:
148
+ ok = client.login()
149
+ if ok:
150
+ success("登录成功! 🎉")
151
+ else:
152
+ warning("登录超时,请稍后重试: dy login")
153
+ except PlaywrightError as e:
154
+ error(f"登录失败: {e}")
155
+ info("稍后使用 [bold]dy login[/] 重试")
156
+ except Exception as e:
157
+ error(f"登录失败: {e}")
158
+ info("请确保已安装 Chromium: playwright install chromium")
159
+
160
+ # ── 完成 ─────────────────────────────────────────────
161
+ console.print()
162
+ console.rule("[bold green]✅ 初始化完成[/]")
163
+ console.print()
164
+ console.print(Panel(
165
+ "[bold]🎉 你已准备就绪! 以下是常用命令:[/]\n\n"
166
+ " [bold cyan]dy search[/] \"关键词\" 搜索视频\n"
167
+ " [bold cyan]dy trending[/] 抖音热榜\n"
168
+ " [bold cyan]dy download[/] URL 无水印下载\n"
169
+ " [bold cyan]dy publish[/] -t 标题 -c 描述 -v 视频 发布视频\n"
170
+ " [bold cyan]dy detail[/] AWEME_ID 视频详情\n"
171
+ " [bold cyan]dy analytics[/] 数据看板\n"
172
+ " [bold cyan]dy me[/] 查看我的信息\n"
173
+ " [bold cyan]dy --help[/] 查看所有命令\n\n"
174
+ "[dim]提示: 大部分命令支持 --json-output 输出 JSON 格式[/]",
175
+ title="📖 快速参考",
176
+ border_style="cyan",
177
+ ))
178
+ console.print()
179
+
180
+
181
+ def _check_playwright() -> bool:
182
+ """检查 playwright 是否已安装。"""
183
+ try:
184
+ import playwright
185
+ return True
186
+ except ImportError:
187
+ return False
188
+
189
+
190
+ def _check_chromium() -> bool:
191
+ """检查 Playwright Chromium 是否已安装。"""
192
+ try:
193
+ result = subprocess.run(
194
+ [sys.executable, "-m", "playwright", "install", "--dry-run", "chromium"],
195
+ capture_output=True,
196
+ text=True,
197
+ )
198
+ return result.returncode == 0
199
+ except Exception:
200
+ return False
@@ -0,0 +1,140 @@
1
+ """
2
+ dy like / comment / favorite / follow — 互动命令 (Playwright)。
3
+ """
4
+ from __future__ import annotations
5
+
6
+ import click
7
+
8
+ from dy_cli.engines.playwright_client import PlaywrightClient, PlaywrightError
9
+ from dy_cli.utils.index_cache import resolve_id
10
+ from dy_cli.utils.output import success, error, info, console, print_comments
11
+
12
+
13
+ def _resolve(id_str: str) -> str:
14
+ try:
15
+ return resolve_id(id_str)
16
+ except ValueError as e:
17
+ error(str(e))
18
+ raise SystemExit(1)
19
+
20
+
21
+ def _pw(account=None) -> PlaywrightClient:
22
+ return PlaywrightClient(account=account, headless=True)
23
+
24
+
25
+ @click.command("like", help="点赞视频 (支持短索引: dy like 1)")
26
+ @click.argument("aweme_id")
27
+ @click.option("--unlike", is_flag=True, help="取消点赞")
28
+ @click.option("--account", default=None, help="使用指定账号")
29
+ def like(aweme_id, unlike, account):
30
+ """点赞或取消点赞。"""
31
+ aweme_id = _resolve(aweme_id)
32
+ action = "unlike" if unlike else "like"
33
+ action_cn = "取消点赞" if unlike else "点赞"
34
+ info(f"正在{action_cn}: {aweme_id}")
35
+
36
+ try:
37
+ result = _pw(account).interact(aweme_id, action)
38
+ if result.get("success"):
39
+ success(f"{action_cn}成功 👍")
40
+ else:
41
+ error(f"{action_cn}失败: 未找到按钮")
42
+ raise SystemExit(1)
43
+ except PlaywrightError as e:
44
+ error(f"{action_cn}失败: {e}")
45
+ raise SystemExit(1)
46
+
47
+
48
+ @click.command("favorite", help="收藏视频 (支持短索引: dy fav 1)")
49
+ @click.argument("aweme_id")
50
+ @click.option("--unfavorite", is_flag=True, help="取消收藏")
51
+ @click.option("--account", default=None, help="使用指定账号")
52
+ def favorite(aweme_id, unfavorite, account):
53
+ """收藏或取消收藏。"""
54
+ aweme_id = _resolve(aweme_id)
55
+ action = "unfavorite" if unfavorite else "favorite"
56
+ action_cn = "取消收藏" if unfavorite else "收藏"
57
+ info(f"正在{action_cn}: {aweme_id}")
58
+
59
+ try:
60
+ result = _pw(account).interact(aweme_id, action)
61
+ if result.get("success"):
62
+ success(f"{action_cn}成功 ⭐")
63
+ else:
64
+ error(f"{action_cn}失败: 未找到按钮")
65
+ raise SystemExit(1)
66
+ except PlaywrightError as e:
67
+ error(f"{action_cn}失败: {e}")
68
+ raise SystemExit(1)
69
+
70
+
71
+ @click.command("comment", help="评论视频 (支持短索引: dy comment 1 -c '好看')")
72
+ @click.argument("aweme_id")
73
+ @click.option("--content", "-c", required=True, help="评论内容")
74
+ @click.option("--account", default=None, help="使用指定账号")
75
+ def comment(aweme_id, content, account):
76
+ """发表评论。"""
77
+ aweme_id = _resolve(aweme_id)
78
+ info(f"正在评论: {aweme_id}")
79
+
80
+ try:
81
+ result = _pw(account).interact(aweme_id, "comment", content=content)
82
+ if result.get("success"):
83
+ success("评论成功 💬")
84
+ else:
85
+ error("评论失败: 未找到输入框")
86
+ raise SystemExit(1)
87
+ except PlaywrightError as e:
88
+ error(f"评论失败: {e}")
89
+ raise SystemExit(1)
90
+
91
+
92
+ @click.command("comments", help="查看视频评论 (支持短索引)")
93
+ @click.argument("aweme_id")
94
+ @click.option("--count", type=int, default=20, help="评论数量")
95
+ @click.option("--account", default=None, help="使用指定账号")
96
+ @click.option("--json-output", "as_json", is_flag=True, help="输出 JSON")
97
+ def comments(aweme_id, count, account, as_json):
98
+ """查看视频评论列表。"""
99
+ aweme_id = _resolve(aweme_id)
100
+ from dy_cli.engines.api_client import DouyinAPIClient, DouyinAPIError
101
+ client = DouyinAPIClient.from_config(account)
102
+
103
+ try:
104
+ info(f"正在获取评论: {aweme_id}")
105
+ data = client.get_comments(aweme_id, count=count)
106
+ comment_list = data.get("comments", [])
107
+
108
+ if as_json:
109
+ from dy_cli.utils.output import print_json
110
+ print_json(data)
111
+ else:
112
+ print_comments(comment_list)
113
+
114
+ except DouyinAPIError as e:
115
+ error(f"获取评论失败: {e}")
116
+ raise SystemExit(1)
117
+ finally:
118
+ client.close()
119
+
120
+
121
+ @click.command("follow", help="关注用户")
122
+ @click.argument("sec_user_id")
123
+ @click.option("--unfollow", is_flag=True, help="取消关注")
124
+ @click.option("--account", default=None, help="使用指定账号")
125
+ def follow(sec_user_id, unfollow, account):
126
+ """关注或取消关注用户。"""
127
+ action = "unfollow" if unfollow else "follow"
128
+ action_cn = "取消关注" if unfollow else "关注"
129
+ info(f"正在{action_cn}用户")
130
+
131
+ try:
132
+ result = _pw(account).interact("", action, sec_user_id=sec_user_id)
133
+ if result.get("success"):
134
+ success(f"{action_cn}成功 👥")
135
+ else:
136
+ error(f"{action_cn}失败: 未找到按钮")
137
+ raise SystemExit(1)
138
+ except PlaywrightError as e:
139
+ error(f"{action_cn}失败: {e}")
140
+ raise SystemExit(1)
@@ -0,0 +1,141 @@
1
+ """
2
+ dy live — 直播相关命令(抖音特色功能)。
3
+ """
4
+ from __future__ import annotations
5
+
6
+ import os
7
+ import subprocess
8
+ import shutil
9
+
10
+ import click
11
+
12
+ from dy_cli.engines.api_client import DouyinAPIClient, DouyinAPIError
13
+ from dy_cli.utils import config
14
+ from dy_cli.utils.output import success, error, info, warning, console, print_json, print_live_info
15
+
16
+
17
+ @click.group("live", help="📺 直播功能 (查看/录制)")
18
+ def live_group():
19
+ pass
20
+
21
+
22
+ @live_group.command("info", help="查看直播间信息")
23
+ @click.argument("room_id")
24
+ @click.option("--account", default=None, help="使用指定账号")
25
+ @click.option("--json-output", "as_json", is_flag=True, help="输出 JSON")
26
+ def live_info(room_id, account, as_json):
27
+ """查看直播间信息(观众数、主播、拉流地址)。"""
28
+ client = DouyinAPIClient.from_config(account)
29
+
30
+ try:
31
+ info(f"正在获取直播间信息: {room_id}")
32
+ data = client.get_live_info(room_id)
33
+
34
+ if as_json:
35
+ print_json(data)
36
+ else:
37
+ print_live_info(data)
38
+
39
+ # Show stream URLs if available
40
+ stream_data = data.get("stream_url", {})
41
+ if isinstance(stream_data, dict):
42
+ flv_url = stream_data.get("flv_pull_url", {})
43
+ hls_url = stream_data.get("hls_pull_url_map", {})
44
+ if flv_url or hls_url:
45
+ console.print()
46
+ info("拉流地址:")
47
+ for quality, url in (flv_url or hls_url).items():
48
+ console.print(f" [{quality}] {url[:80]}…" if len(url) > 80 else f" [{quality}] {url}")
49
+
50
+ except DouyinAPIError as e:
51
+ error(f"获取直播信息失败: {e}")
52
+ raise SystemExit(1)
53
+ finally:
54
+ client.close()
55
+
56
+
57
+ @live_group.command("record", help="录制直播")
58
+ @click.argument("room_id")
59
+ @click.option("--output", "-o", default=None, help="输出文件路径")
60
+ @click.option("--quality", type=click.Choice(["FULL_HD1", "HD1", "SD1", "SD2"]),
61
+ default="FULL_HD1", help="画质 (默认最高)")
62
+ @click.option("--account", default=None, help="使用指定账号")
63
+ def live_record(room_id, output, quality, account):
64
+ """
65
+ 录制直播视频 (需要 ffmpeg)。
66
+
67
+ 使用 ffmpeg 拉流保存,Ctrl+C 停止录制。
68
+ """
69
+ # Check ffmpeg
70
+ if not shutil.which("ffmpeg"):
71
+ error("需要安装 ffmpeg: brew install ffmpeg (macOS)")
72
+ raise SystemExit(1)
73
+
74
+ client = DouyinAPIClient.from_config(account)
75
+
76
+ try:
77
+ info(f"正在获取直播拉流地址: {room_id}")
78
+ data = client.get_live_info(room_id)
79
+
80
+ # Check if live
81
+ status_val = data.get("status")
82
+ if status_val != 2:
83
+ error("直播间未开播或已结束")
84
+ raise SystemExit(1)
85
+
86
+ # Get stream URL
87
+ stream_data = data.get("stream_url", {})
88
+ flv_urls = stream_data.get("flv_pull_url", {})
89
+ hls_urls = stream_data.get("hls_pull_url_map", {})
90
+
91
+ stream_url = None
92
+ urls = flv_urls or hls_urls
93
+ if isinstance(urls, dict):
94
+ # Try preferred quality, fallback to any available
95
+ stream_url = urls.get(quality) or next(iter(urls.values()), None)
96
+
97
+ if not stream_url:
98
+ error("未获取到拉流地址")
99
+ raise SystemExit(1)
100
+
101
+ # Output file
102
+ if not output:
103
+ cfg = config.load_config()
104
+ dl_dir = cfg["default"].get("download_dir", os.path.expanduser("~/Downloads/douyin"))
105
+ os.makedirs(dl_dir, exist_ok=True)
106
+ owner = data.get("owner", {}).get("nickname", room_id)
107
+ import datetime
108
+ ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
109
+ output = os.path.join(dl_dir, f"live_{owner}_{ts}.mp4")
110
+
111
+ info(f"开始录制: {output}")
112
+ info("按 Ctrl+C 停止录制")
113
+ console.print()
114
+
115
+ # Use ffmpeg to record
116
+ cmd = [
117
+ "ffmpeg",
118
+ "-i", stream_url,
119
+ "-c", "copy",
120
+ "-movflags", "+faststart",
121
+ output,
122
+ ]
123
+
124
+ try:
125
+ subprocess.run(cmd, check=True)
126
+ except KeyboardInterrupt:
127
+ console.print()
128
+ if os.path.isfile(output):
129
+ size = os.path.getsize(output)
130
+ size_str = f"{size / 1024 / 1024:.1f}MB"
131
+ success(f"录制完成: {output} ({size_str})")
132
+ else:
133
+ warning("录制已取消")
134
+ except subprocess.CalledProcessError as e:
135
+ error(f"ffmpeg 录制失败: {e}")
136
+
137
+ except DouyinAPIError as e:
138
+ error(f"获取直播信息失败: {e}")
139
+ raise SystemExit(1)
140
+ finally:
141
+ client.close()
@@ -0,0 +1,78 @@
1
+ """
2
+ dy me / profile — 用户信息命令。
3
+ """
4
+ from __future__ import annotations
5
+
6
+ import click
7
+
8
+ from dy_cli.engines.api_client import DouyinAPIClient, DouyinAPIError
9
+ from dy_cli.engines.playwright_client import PlaywrightClient, PlaywrightError
10
+ from dy_cli.utils import config
11
+ from dy_cli.utils.output import (
12
+ success, error, info, console,
13
+ print_user_profile, print_json, print_videos,
14
+ )
15
+
16
+
17
+ @click.command("me", help="查看自己的账号信息")
18
+ @click.option("--account", default=None, help="账号名")
19
+ @click.option("--json-output", "as_json", is_flag=True, help="输出 JSON")
20
+ def me(account, as_json):
21
+ """查看当前登录账号信息。"""
22
+ client = PlaywrightClient(account=account, headless=True)
23
+
24
+ if not client.cookie_exists():
25
+ error("未登录,请先运行: dy login")
26
+ raise SystemExit(1)
27
+
28
+ info("正在检查登录状态...")
29
+ try:
30
+ logged_in = client.check_login()
31
+ if logged_in:
32
+ success("已登录抖音 ✅")
33
+ console.print(f" [bold]Cookie:[/] {client.cookie_file}")
34
+ else:
35
+ error("Cookie 已失效,请重新登录: dy login")
36
+ raise SystemExit(1)
37
+ except PlaywrightError as e:
38
+ error(f"检查失败: {e}")
39
+ raise SystemExit(1)
40
+
41
+
42
+ @click.command("profile", help="查看用户主页")
43
+ @click.argument("sec_user_id")
44
+ @click.option("--posts", is_flag=True, help="同时加载作品列表")
45
+ @click.option("--post-count", type=int, default=20, help="作品数量 (默认 20)")
46
+ @click.option("--account", default=None, help="使用指定账号")
47
+ @click.option("--json-output", "as_json", is_flag=True, help="输出 JSON")
48
+ def profile(sec_user_id, posts, post_count, account, as_json):
49
+ """查看用户主页信息和作品。"""
50
+ client = DouyinAPIClient.from_config(account)
51
+
52
+ try:
53
+ info(f"正在获取用户资料...")
54
+ user = client.get_user_profile(sec_user_id)
55
+
56
+ if as_json and not posts:
57
+ print_json(user)
58
+ return
59
+
60
+ print_user_profile(user)
61
+
62
+ # Load posts
63
+ if posts:
64
+ info("正在获取作品列表...")
65
+ post_data = client.get_user_posts(sec_user_id, count=post_count)
66
+ aweme_list = post_data.get("aweme_list", [])
67
+
68
+ if as_json:
69
+ print_json({"user": user, "posts": aweme_list})
70
+ else:
71
+ nickname = user.get("nickname", "")
72
+ print_videos(aweme_list, keyword=f"{nickname} 的作品")
73
+
74
+ except DouyinAPIError as e:
75
+ error(f"获取用户资料失败: {e}")
76
+ raise SystemExit(1)
77
+ finally:
78
+ client.close()
@@ -0,0 +1,123 @@
1
+ """
2
+ dy publish — 发布命令 (Playwright 引擎)。
3
+ """
4
+ from __future__ import annotations
5
+
6
+ import os
7
+
8
+ import click
9
+
10
+ from dy_cli.engines.playwright_client import PlaywrightClient, PlaywrightError
11
+ from dy_cli.utils import config
12
+ from dy_cli.utils.output import success, error, info, warning, console
13
+
14
+
15
+ @click.command("publish", help="发布视频或图文到抖音")
16
+ @click.option("--title", "-t", required=True, help="标题")
17
+ @click.option("--content", "-c", default=None, help="描述正文")
18
+ @click.option("--content-file", type=click.Path(exists=True), default=None, help="从文件读取描述")
19
+ @click.option("--video", "-v", default=None, help="视频文件路径")
20
+ @click.option("--images", "-i", multiple=True, help="图片路径 (可多个)")
21
+ @click.option("--tags", multiple=True, help="标签 (可多个,如: --tags 旅行 --tags 美食)")
22
+ @click.option("--visibility", type=click.Choice(["公开", "好友可见", "仅自己可见"]),
23
+ default="公开", help="可见范围")
24
+ @click.option("--schedule", default=None, help="定时发布 (ISO 8601, 如 2026-03-16T10:00:00+08:00)")
25
+ @click.option("--thumbnail", default=None, type=click.Path(exists=True), help="封面图片路径")
26
+ @click.option("--account", default=None, help="使用指定账号")
27
+ @click.option("--headless", is_flag=True, help="无头模式 (不显示浏览器)")
28
+ @click.option("--dry-run", is_flag=True, help="预览模式,不实际发布")
29
+ def publish(title, content, content_file, video, images, tags, visibility, schedule, thumbnail, account, headless, dry_run):
30
+ """发布视频或图文。"""
31
+
32
+ # Handle content
33
+ if content_file:
34
+ with open(content_file, "r", encoding="utf-8") as f:
35
+ content = f.read().strip()
36
+ if not content:
37
+ content = ""
38
+
39
+ # Validate media
40
+ images = list(images)
41
+ if not images and not video:
42
+ error("必须提供视频 (--video) 或图片 (--images)")
43
+ raise SystemExit(1)
44
+
45
+ if video and images:
46
+ error("不能同时提供视频和图片,请选择一种")
47
+ raise SystemExit(1)
48
+
49
+ # Validate files
50
+ if video and not video.startswith("http") and not os.path.isfile(video):
51
+ error(f"视频文件不存在: {video}")
52
+ raise SystemExit(1)
53
+
54
+ for img in images:
55
+ if not img.startswith("http") and not os.path.isfile(img):
56
+ error(f"图片文件不存在: {img}")
57
+ raise SystemExit(1)
58
+
59
+ tags = list(tags)
60
+
61
+ # Dry run
62
+ if dry_run:
63
+ console.print()
64
+ info("📋 发布预览:")
65
+ console.print(f" [bold]标题:[/] {title}")
66
+ console.print(f" [bold]描述:[/] {content[:100]}{'...' if len(content) > 100 else ''}")
67
+ if video:
68
+ console.print(f" [bold]视频:[/] {video}")
69
+ else:
70
+ console.print(f" [bold]图片:[/] {', '.join(images)}")
71
+ if tags:
72
+ console.print(f" [bold]标签:[/] {', '.join(tags)}")
73
+ console.print(f" [bold]可见:[/] {visibility}")
74
+ if schedule:
75
+ console.print(f" [bold]定时:[/] {schedule}")
76
+ if thumbnail:
77
+ console.print(f" [bold]封面:[/] {thumbnail}")
78
+ console.print()
79
+ return
80
+
81
+ # Publish
82
+ cfg = config.load_config()
83
+ use_headless = headless or cfg["playwright"].get("headless", False)
84
+
85
+ client = PlaywrightClient(
86
+ account=account,
87
+ headless=use_headless,
88
+ slow_mo=cfg["playwright"].get("slow_mo", 0),
89
+ )
90
+
91
+ if not client.cookie_exists():
92
+ error("未登录,请先运行: dy login")
93
+ raise SystemExit(1)
94
+
95
+ try:
96
+ if video:
97
+ info(f"正在发布视频: {os.path.basename(video)}")
98
+ result = client.publish_video(
99
+ title=title,
100
+ content=content,
101
+ video_path=os.path.abspath(video),
102
+ tags=tags or None,
103
+ visibility=visibility,
104
+ schedule_at=schedule,
105
+ thumbnail_path=thumbnail,
106
+ )
107
+ else:
108
+ info(f"正在发布图文 ({len(images)} 张图片)")
109
+ result = client.publish_image_text(
110
+ title=title,
111
+ content=content,
112
+ images=[os.path.abspath(img) if not img.startswith("http") else img for img in images],
113
+ tags=tags or None,
114
+ visibility=visibility,
115
+ schedule_at=schedule,
116
+ )
117
+
118
+ success("发布成功! 🎉")
119
+ info("提示: 可用 [bold]dy search[/] 搜索验证发布状态")
120
+
121
+ except PlaywrightError as e:
122
+ error(f"发布失败: {e}")
123
+ raise SystemExit(1)