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,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()