Implement basic Jianying_controller to conduct automatic exporting

This commit is contained in:
gary318 2024-10-04 10:33:11 +08:00
parent 32f1cbff2f
commit da1b6a78c8
No known key found for this signature in database
GPG Key ID: C13F9DB85F47961A
6 changed files with 176 additions and 2 deletions

View File

@ -20,8 +20,10 @@
- ☑️ [修改文本片段的文本内容](#替换文本片段的内容)
### 批量导出
- ⬜ 控制剪映打开指定草稿
- ⬜ 控制剪映导出草稿至指定位置
- ☑️ 控制剪映打开指定草稿
- ☑️ 导出草稿至指定位置
- ⬜ 调节部分导出参数
- 感谢`@litter jump`提供部分思路
### 视频与图片
- ☑️ 添加本地视频/图片素材

View File

@ -17,6 +17,7 @@ from .track import Track_type
from .template_mode import Shrink_mode, Extend_mode
from .script_file import Script_file
from .draft_folder import Draft_folder
from .jianying_controller import Jianying_controller
from .time_util import SEC, tim, trange
@ -50,6 +51,7 @@ __all__ = [
"Extend_mode",
"Script_file",
"Draft_folder",
"Jianying_controller",
"SEC",
"tim",
"trange"

View File

@ -14,3 +14,10 @@ class AmbiguousMaterial(ValueError):
class ExtensionFailed(ValueError):
"""替换素材时延伸片段失败"""
class DraftNotFound(NameError):
"""未找到草稿"""
class AutomationError(Exception):
"""自动化操作失败"""
class ExportTimeout(Exception):
"""导出超时"""

View File

@ -0,0 +1,162 @@
"""剪映自动化控制,主要与自动导出有关"""
import time
import shutil
import uiautomation as uia
from typing import Optional, Literal
from . import exceptions
from .exceptions import AutomationError
class Jianying_controller:
"""剪映控制器"""
app: uia.WindowControl
"""剪映窗口"""
app_status: Literal["home", "edit", "pre_export"]
def __init__(self):
"""初始化剪映控制器, 此时剪映应该处于目录页"""
self.get_window()
def export_draft(self, draft_name: str, output_dir: Optional[str] = None, timeout: float = 1e9) -> None:
"""导出指定的剪映草稿
**注意: 需要确认有导出草稿的权限(不使用VIP功能或已开通VIP), 否则可能陷入死循环**
Args:
draft_name (`str`): 要导出的剪映草稿名称
output_path (`str`, optional): 导出路径, 导出完成后会将文件剪切到此, 不指定则使用剪映默认路径.
timeout (`float`, optional): 导出超时时间(), 默认无限制.
Raises:
`DraftNotFound`: 未找到指定名称的剪映草稿
`AutomationError`: 剪映操作失败
"""
self.switch_to_home()
# 点击对应草稿
draft_name_text = self.app.TextControl(searchDepth=2,
Compare=lambda ctrl, depth: self.__draft_name_cmp(draft_name, ctrl, depth))
if not draft_name_text.Exists(0):
raise exceptions.DraftNotFound(f"未找到名为{draft_name}的剪映草稿")
draft_btn = draft_name_text.GetParentControl()
assert draft_btn is not None
draft_btn.Click(simulateMove=False)
time.sleep(10)
self.get_window()
# 点击导出按钮
export_btn = self.app.TextControl(searchDepth=2, Compare=self.__edit_page_export_cmp)
if not export_btn.Exists(0):
raise AutomationError("未找到导出按钮")
export_btn.Click(simulateMove=False)
time.sleep(3)
self.get_window()
# 获取原始导出路径
export_path_sib = self.app.TextControl(searchDepth=2, Compare=self.__export_path_cmp)
if not export_path_sib.Exists(0):
raise AutomationError("未找到导出路径框")
export_path_text = export_path_sib.GetSiblingControl(lambda ctrl: True)
assert export_path_text is not None
export_path = export_path_text.GetPropertyValue(30159)
# 点击导出
export_btn = self.app.TextControl(searchDepth=2, Compare=self.__export_btn_cmp)
if not export_btn.Exists(0):
raise AutomationError("未找到导出按钮")
export_btn.Click(simulateMove=False)
time.sleep(2)
# 等待导出完成
st = time.time()
while True:
self.get_window()
if self.app_status != "pre_export": continue
export_window = self.app.WindowControl(searchDepth=1, Name="JianyingPro")
if export_window.Exists(0):
close_btn = export_window.ButtonControl(Name="关闭")
if close_btn.Exists(1):
close_btn.Click(simulateMove=False)
break
if time.time() - st > timeout:
raise AutomationError("导出超时, 时限为%d" % timeout)
time.sleep(1)
time.sleep(2)
# 复制导出的文件到指定目录
if output_dir is not None:
shutil.move(export_path, output_dir)
def switch_to_home(self) -> None:
"""切换到剪映主页"""
if self.app_status == "home":
return
if self.app_status != "edit":
raise AutomationError("仅支持从编辑模式切换到主页")
close_btn = self.app.GroupControl(searchDepth=1, ClassName="TitleBarButton", foundIndex=3)
close_btn.Click(simulateMove=False)
time.sleep(2)
self.get_window()
def get_window(self) -> None:
"""寻找剪映窗口并置顶"""
if hasattr(self, "app") and self.app.Exists(0):
self.app.SetTopmost(False)
self.app = uia.WindowControl(searchDepth=1, Compare=self.__jianying_window_cmp)
if not self.app.Exists(0):
raise AutomationError("剪映窗口未找到")
# 寻找可能存在的导出窗口
export_window = self.app.WindowControl(searchDepth=1, Name="导出")
if export_window.Exists(0):
self.app = export_window
self.app_status = "pre_export"
self.app.SetActive()
self.app.SetTopmost()
def __jianying_window_cmp(self, control: uia.WindowControl, depth: int) -> bool:
if control.Name != "剪映专业版":
return False
if "HomePage".lower() in control.ClassName.lower():
self.app_status = "home"
return True
if "MainWindow".lower() in control.ClassName.lower():
self.app_status = "edit"
return True
return False
@staticmethod
def __draft_name_cmp(draft_name: str, control: uia.TextControl, depth: int) -> bool:
if depth != 2:
return False
full_desc: str = control.GetPropertyValue(30159)
return "Title:".lower() in full_desc.lower() and draft_name in full_desc
@staticmethod
def __edit_page_export_cmp(control: uia.TextControl, depth: int) -> bool:
if depth != 2:
return False
full_desc: str = control.GetPropertyValue(30159).lower()
return "title" in full_desc and "export" in full_desc
@staticmethod
def __export_btn_cmp(control: uia.TextControl, depth: int) -> bool:
if depth != 2:
return False
full_desc: str = control.GetPropertyValue(30159).lower()
return "ExportOkBtn".lower() == full_desc
@staticmethod
def __export_path_cmp(control: uia.TextControl, depth: int) -> bool:
if depth != 2:
return False
full_desc: str = control.GetPropertyValue(30159).lower()
return "ExportPath".lower() in full_desc

Binary file not shown.

Before

Width:  |  Height:  |  Size: 209 KiB

After

Width:  |  Height:  |  Size: 207 KiB

View File

@ -1,2 +1,3 @@
pymediainfo
imageio
uiautomation>=2