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.
- dy_cli/__init__.py +3 -0
- dy_cli/commands/__init__.py +0 -0
- dy_cli/commands/account.py +103 -0
- dy_cli/commands/analytics.py +120 -0
- dy_cli/commands/auth.py +159 -0
- dy_cli/commands/config_cmd.py +67 -0
- dy_cli/commands/download.py +212 -0
- dy_cli/commands/init.py +200 -0
- dy_cli/commands/interact.py +140 -0
- dy_cli/commands/live.py +141 -0
- dy_cli/commands/profile.py +78 -0
- dy_cli/commands/publish.py +123 -0
- dy_cli/commands/search.py +131 -0
- dy_cli/commands/trending.py +82 -0
- dy_cli/engines/__init__.py +0 -0
- dy_cli/engines/api_client.py +665 -0
- dy_cli/engines/playwright_client.py +836 -0
- dy_cli/main.py +144 -0
- dy_cli/utils/__init__.py +0 -0
- dy_cli/utils/config.py +99 -0
- dy_cli/utils/envelope.py +49 -0
- dy_cli/utils/export.py +68 -0
- dy_cli/utils/index_cache.py +83 -0
- dy_cli/utils/output.py +283 -0
- dy_cli/utils/signature.py +183 -0
- dy_cli-0.2.0.dist-info/METADATA +376 -0
- dy_cli-0.2.0.dist-info/RECORD +34 -0
- dy_cli-0.2.0.dist-info/WHEEL +4 -0
- dy_cli-0.2.0.dist-info/entry_points.txt +2 -0
- dy_cli-0.2.0.dist-info/licenses/LICENSE +21 -0
- scripts/chrome_launcher.py +71 -0
- scripts/douyin_analytics.py +99 -0
- scripts/douyin_login.py +64 -0
- scripts/douyin_publisher.py +199 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""
|
|
2
|
+
抖音发布脚本 — Playwright 操控 creator.douyin.com 上传视频/图文。
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
python scripts/douyin_publisher.py --title "标题" --content "描述" --video video.mp4
|
|
6
|
+
python scripts/douyin_publisher.py --title "标题" --content "描述" --images img1.jpg img2.jpg
|
|
7
|
+
python scripts/douyin_publisher.py --title "标题" --content "描述" --video video.mp4 --account work
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import asyncio
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def publish_video(
|
|
18
|
+
title: str,
|
|
19
|
+
content: str,
|
|
20
|
+
video_path: str,
|
|
21
|
+
tags: list[str] | None = None,
|
|
22
|
+
account: str = "default",
|
|
23
|
+
headless: bool = False,
|
|
24
|
+
):
|
|
25
|
+
"""发布视频到抖音创作者中心。"""
|
|
26
|
+
from playwright.async_api import async_playwright
|
|
27
|
+
|
|
28
|
+
cookie_file = os.path.expanduser(f"~/.dy/cookies/{account}.json")
|
|
29
|
+
if not os.path.isfile(cookie_file):
|
|
30
|
+
print(f"[dy] Cookie 文件不存在: {cookie_file}")
|
|
31
|
+
print("[dy] 请先运行: python scripts/douyin_login.py")
|
|
32
|
+
return False
|
|
33
|
+
|
|
34
|
+
async with async_playwright() as pw:
|
|
35
|
+
browser = await pw.chromium.launch(headless=headless)
|
|
36
|
+
context = await browser.new_context(storage_state=cookie_file)
|
|
37
|
+
page = await context.new_page()
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
print("[dy] 正在打开上传页面...")
|
|
41
|
+
await page.goto(
|
|
42
|
+
"https://creator.douyin.com/creator-micro/content/upload",
|
|
43
|
+
wait_until="domcontentloaded",
|
|
44
|
+
)
|
|
45
|
+
await page.wait_for_timeout(3000)
|
|
46
|
+
|
|
47
|
+
# Check login
|
|
48
|
+
if await page.get_by_text("扫码登录").count() > 0:
|
|
49
|
+
print("[dy] Cookie 已失效,请重新登录")
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
# Upload video
|
|
53
|
+
print(f"[dy] 上传视频: {os.path.basename(video_path)}")
|
|
54
|
+
upload_input = page.locator('input[type="file"]').first
|
|
55
|
+
await upload_input.set_input_files(os.path.abspath(video_path))
|
|
56
|
+
|
|
57
|
+
# Wait for upload
|
|
58
|
+
print("[dy] 等待上传完成...")
|
|
59
|
+
for _ in range(120):
|
|
60
|
+
ready = await page.locator('[contenteditable="true"]').count()
|
|
61
|
+
if ready > 0:
|
|
62
|
+
break
|
|
63
|
+
await page.wait_for_timeout(5000)
|
|
64
|
+
|
|
65
|
+
# Fill content
|
|
66
|
+
editor = page.locator('[contenteditable="true"]').first
|
|
67
|
+
await editor.click()
|
|
68
|
+
|
|
69
|
+
full_text = content
|
|
70
|
+
if tags:
|
|
71
|
+
full_text += " " + " ".join(f"#{t}" for t in tags)
|
|
72
|
+
|
|
73
|
+
await page.keyboard.type(full_text, delay=50)
|
|
74
|
+
print("[dy] 内容已填写")
|
|
75
|
+
|
|
76
|
+
# Click publish
|
|
77
|
+
await page.wait_for_timeout(2000)
|
|
78
|
+
publish_btn = page.locator('button:has-text("发布")').first
|
|
79
|
+
try:
|
|
80
|
+
await publish_btn.click()
|
|
81
|
+
await page.wait_for_timeout(5000)
|
|
82
|
+
print("[dy] ✅ 发布成功!")
|
|
83
|
+
return True
|
|
84
|
+
except Exception:
|
|
85
|
+
print("[dy] 未找到发布按钮,请手动确认")
|
|
86
|
+
if not headless:
|
|
87
|
+
await page.wait_for_timeout(30000)
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
finally:
|
|
91
|
+
await context.storage_state(path=cookie_file)
|
|
92
|
+
await browser.close()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
async def publish_images(
|
|
96
|
+
title: str,
|
|
97
|
+
content: str,
|
|
98
|
+
images: list[str],
|
|
99
|
+
tags: list[str] | None = None,
|
|
100
|
+
account: str = "default",
|
|
101
|
+
headless: bool = False,
|
|
102
|
+
):
|
|
103
|
+
"""发布图文到抖音创作者中心。"""
|
|
104
|
+
from playwright.async_api import async_playwright
|
|
105
|
+
|
|
106
|
+
cookie_file = os.path.expanduser(f"~/.dy/cookies/{account}.json")
|
|
107
|
+
if not os.path.isfile(cookie_file):
|
|
108
|
+
print(f"[dy] Cookie 文件不存在: {cookie_file}")
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
async with async_playwright() as pw:
|
|
112
|
+
browser = await pw.chromium.launch(headless=headless)
|
|
113
|
+
context = await browser.new_context(storage_state=cookie_file)
|
|
114
|
+
page = await context.new_page()
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
await page.goto(
|
|
118
|
+
"https://creator.douyin.com/creator-micro/content/upload",
|
|
119
|
+
wait_until="domcontentloaded",
|
|
120
|
+
)
|
|
121
|
+
await page.wait_for_timeout(3000)
|
|
122
|
+
|
|
123
|
+
if await page.get_by_text("扫码登录").count() > 0:
|
|
124
|
+
print("[dy] Cookie 已失效")
|
|
125
|
+
return False
|
|
126
|
+
|
|
127
|
+
# Switch to image tab
|
|
128
|
+
try:
|
|
129
|
+
img_tab = page.locator('text=图文').first
|
|
130
|
+
if await img_tab.count() > 0:
|
|
131
|
+
await img_tab.click()
|
|
132
|
+
await page.wait_for_timeout(1000)
|
|
133
|
+
except Exception:
|
|
134
|
+
pass
|
|
135
|
+
|
|
136
|
+
# Upload images
|
|
137
|
+
abs_images = [os.path.abspath(img) for img in images]
|
|
138
|
+
upload_input = page.locator('input[type="file"]').first
|
|
139
|
+
await upload_input.set_input_files(abs_images)
|
|
140
|
+
print(f"[dy] 上传 {len(abs_images)} 张图片")
|
|
141
|
+
await page.wait_for_timeout(3000)
|
|
142
|
+
|
|
143
|
+
# Fill content
|
|
144
|
+
editor = page.locator('[contenteditable="true"]').first
|
|
145
|
+
await editor.click()
|
|
146
|
+
|
|
147
|
+
full_text = content
|
|
148
|
+
if tags:
|
|
149
|
+
full_text += " " + " ".join(f"#{t}" for t in tags)
|
|
150
|
+
|
|
151
|
+
await page.keyboard.type(full_text, delay=50)
|
|
152
|
+
|
|
153
|
+
# Publish
|
|
154
|
+
await page.wait_for_timeout(2000)
|
|
155
|
+
publish_btn = page.locator('button:has-text("发布")').first
|
|
156
|
+
try:
|
|
157
|
+
await publish_btn.click()
|
|
158
|
+
await page.wait_for_timeout(5000)
|
|
159
|
+
print("[dy] ✅ 发布成功!")
|
|
160
|
+
return True
|
|
161
|
+
except Exception:
|
|
162
|
+
print("[dy] 请手动确认发布")
|
|
163
|
+
return False
|
|
164
|
+
|
|
165
|
+
finally:
|
|
166
|
+
await context.storage_state(path=cookie_file)
|
|
167
|
+
await browser.close()
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def main():
|
|
171
|
+
parser = argparse.ArgumentParser(description="抖音内容发布")
|
|
172
|
+
parser.add_argument("--title", "-t", required=True, help="标题")
|
|
173
|
+
parser.add_argument("--content", "-c", required=True, help="描述")
|
|
174
|
+
parser.add_argument("--video", "-v", default=None, help="视频文件路径")
|
|
175
|
+
parser.add_argument("--images", "-i", nargs="+", default=None, help="图片文件路径")
|
|
176
|
+
parser.add_argument("--tags", nargs="+", default=None, help="标签")
|
|
177
|
+
parser.add_argument("--account", default="default", help="账号名")
|
|
178
|
+
parser.add_argument("--headless", action="store_true", help="无头模式")
|
|
179
|
+
args = parser.parse_args()
|
|
180
|
+
|
|
181
|
+
if args.video:
|
|
182
|
+
ok = asyncio.run(publish_video(
|
|
183
|
+
args.title, args.content, args.video,
|
|
184
|
+
tags=args.tags, account=args.account, headless=args.headless,
|
|
185
|
+
))
|
|
186
|
+
elif args.images:
|
|
187
|
+
ok = asyncio.run(publish_images(
|
|
188
|
+
args.title, args.content, args.images,
|
|
189
|
+
tags=args.tags, account=args.account, headless=args.headless,
|
|
190
|
+
))
|
|
191
|
+
else:
|
|
192
|
+
print("[dy] 请指定 --video 或 --images")
|
|
193
|
+
sys.exit(1)
|
|
194
|
+
|
|
195
|
+
sys.exit(0 if ok else 1)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
if __name__ == "__main__":
|
|
199
|
+
main()
|