1
1
mirror of https://github.com/KenanZhu/AutoLibrary.git synced 2026-06-18 07:23:03 +08:00

Compare commits

...

46 Commits

Author SHA1 Message Date
github-actions[bot] 43b87db4eb chore(release): v1.0.2 [auto release commit] 2026-01-05 04:05:04 +00:00
KenanZhu ae23f65e5a fix(AutoLib): 修复并完善对不同浏览器驱动的支持,目前支持的浏览器驱动为 Edge、Chrome、Firefox
之前的代码只支持 Edge 浏览器驱动,现在完善了对 Chrome、Firefox 浏览器驱动的支持
2026-01-05 11:59:33 +08:00
KenanZhu a7b9c340ae refactor(ALConfigWidget): 初始化的默认浏览器驱动路径改为空 2026-01-05 11:58:15 +08:00
KenanZhu 96d733d2ed fix(ALConfigWidget): 修复配置界面错误字符 2026-01-05 11:43:16 +08:00
KenanZhu 65cb951ada ci(workflow): 优化 update-version.yml 中的版本信息更新逻辑
对 ALVersionInfo.py 文件的更新逻辑进行差异化处理,分别为 commit-release 和 build 阶段生成不同的 artifact:

* 针对 commit-release 阶段:将所有与构建(build)相关的字段值设置为 'local' 或 'null'
* 针对 build 阶段:保留完整的版本信息字段

这种差异化处理确保其后续提交的 release commit 不会包含构建相关的版本信息。
2026-01-04 10:05:57 +08:00
KenanZhu 94ce3433a3 ci(workflows): 重构 CI/CD 工作流执行配置 2026-01-03 14:33:49 +08:00
github-actions[bot] dd48c8a01c chore(release): v1.0.1 [auto release commit] 2026-01-02 16:39:02 +00:00
KenanZhu 924db3bdcc ci(workflows): 新增基于 Github Actions 的 CI/CD 工作流控制 2026-01-03 00:35:16 +08:00
KenanZhu 1e5452d411 refactor(ALAboutDialog): 更改关于对话框的显示内容
主要包括版本号,提交信息,构建时间等。为 CI/CD 流程添加相关信息占位。
2026-01-03 00:33:01 +08:00
KenanZhu 1b378e5aaa fix(LibLogin): 修复优化验证码处理逻辑,避免无效请求。并完善手动输入验证码功能。 2026-01-02 17:37:17 +08:00
KenanZhu e069efb2ea fix(ALConfigWidget): 修复用户配置列表中,选中用户项时禁用该用户所在用户组时,该用户项未同步禁用状态仍保持被选中的问题 2026-01-02 00:44:24 +08:00
KenanZhu 407d25570a fix(ALMainWorkers): 修复 AutoLibWorker 中基础检查未通过时,运行线程错误返回导致结束信号未发送的问题 2026-01-02 00:30:37 +08:00
KenanZhu bfcb65f56a fix(gui.ALMainWindow): 修改了 setControlButtons 方法,防止按钮状态的意外更改 2025-12-31 10:15:57 +08:00
KenanZhu cde1e966e7 chore(gui.batchs): 将编译脚本的错误命名 complie_*.bat/sh 修改为 compile_*.bat/sh 2025-12-27 23:12:37 +08:00
KenanZhu 8c4f463889 docs(readme): 修改一些文档的不通顺不准确描述,新增捐助链接 2025-12-27 21:57:38 +08:00
KenanZhu 39867cc20c docs(readme): 修改文档的歧义和其它不准确描述 2025-12-27 15:43:25 +08:00
KenanZhu 149910d628 chore(release): v1.0.0 2025-12-22 15:24:31 +08:00
KenanZhu 2a7ed099bf docs(readme.md): 更新 readme 文档 2025-12-22 15:23:56 +08:00
KenanZhu 473f32ca29 chore(batchs): 新增界面资源和应用资源的编译脚本 2025-12-22 15:23:47 +08:00
KenanZhu 580052f1e3 chore(icons): 添加多种图标格式,将当前的图标尺寸从 1024x1024 调整为 32x32 2025-12-22 11:55:33 +08:00
KenanZhu 6abf530307 optimize(ALTimerTaskWidget, ALConfigWidget): 优化定时器和用户设置的任务列表排序 2025-12-22 11:53:45 +08:00
KenanZhu 577c651ef8 feat(ALMainWindow): 引入对新增定时器任务状态 - 执行失败的处理支持 (#18ae949)
同时,为了统一消息处理,我们将 ALMainWorkers 中的原信号
槽处理的消息逻辑更改为使用继承的 MsgBase 类的 showTrace 方法
2025-12-13 14:27:46 +08:00
KenanZhu 18ae949900 feat(ALTimerTaskWidget): 新增定时器任务状态 - 执行失败 2025-12-13 14:22:28 +08:00
KenanZhu ca9059d1db refactor(AutoLib): 初始化 AutoLib 时,发生错误则抛出异常 2025-12-13 14:21:26 +08:00
KenanZhu ad4deae0c6 fix(ALMainWindow): 修复停止时的按钮状态重置问题
函数更改于(#9255eec)
2025-12-13 14:15:28 +08:00
KenanZhu 55ae4d0d96 feat(ALConfigWidget): 大更新 - 用户树状列表和其它
1. 在这个 commit 中,我们思考了许久,最终决定将现有的
用户管理列表转为树状列表,以解决用户数量增多时,用户的
选择性管理,分组等问题。
2. 同时因为该更改需要重构很多内容,我们也在该 commit
中决定将所有‘系统配置’更换为‘运行配置’,同时文件名称和
内容变量也相应变为‘run’和‘user’。
3. 重构 AutoLib 和 ALMainWorkers 中的配置相关代码,
以适应新的用户树状列表。

当前迭代更新至 v1.0.0-beta.4, 同时,在该版本的 rc
阶段前,我们计划不再发布 beta 阶段相关的 release
2025-12-13 00:07:33 +08:00
KenanZhu 7dcd72939b fix(ALMainWindow): fix the wrong use of function 'setControlButtons' 2025-12-12 23:51:54 +08:00
KenanZhu bfce61f4b4 fix(ALTimerTaskWidget): fix timer tasks list is 'NoneType' when init config file 2025-12-12 23:41:30 +08:00
KenanZhu 60a5699822 refactor(ALConfigWidget): ALConfigWidget is changed into non-modal dialog 2025-12-12 18:59:25 +08:00
KenanZhu aab9565012 fix(*): always show the child window on the center of the parent window and do not overflow the screen 2025-12-09 08:54:45 +08:00
KenanZhu 9255eec9f1 style(ALMainWindow): rename some variables and functions 2025-12-09 08:51:14 +08:00
KenanZhu cff6fd8fc0 feat(ALTimerTaskWidget): timer tasks' data persistence and perpetuation 2025-12-09 08:49:44 +08:00
KenanZhu b129f47b48 chore(ALSeat*): rename SeatFrame, SeatMapTable, SeatMapWidget to ALSeatFrame, ALSeatMapTable, ALSeatMapWidget 2025-12-09 08:46:51 +08:00
KenanZhu 069429be71 refactor(ALAboutDialog): replace hide/show methods with 'exec()' for dialog modal handling 2025-12-09 08:19:25 +08:00
KenanZhu 7d064fc8e7 refactor(ALMainWindow): extract the worker threads to a separate file : ALMainWorkers.py 2025-12-09 08:17:39 +08:00
KenanZhu 1b172ad396 style(*): some small style changes 2025-11-30 18:46:12 +08:00
KenanZhu 05c9d433f4 hotfix(LibRenew): fix the serious bug that the renew process always failed
in this hotfix, we fix the renew bug because we
do not refresh the page after renew the seat,
this will make the subsequent operators, such as
logout ... unable to in progress.
2025-11-30 18:42:20 +08:00
KenanZhu 65ca40438d fix(ALMainWindow.ui): fix the convert error of ALMainWindow.ui
we fix the convert error of ALMainWindow.ui by
using pyside6-uic to convert the ui file to python code
2025-11-29 22:31:18 +08:00
KenanZhu 0a8763add5 feat(gui): breaking changes - Timer Task Management
1. we add menu actions 'manual' and 'about', so
you can click actions to open manual and about dialog.
2. we introduce timer task management feature, so
you can add, delete timer tasks to auto run task.
3. other style improvement in gui...
2025-11-29 20:03:45 +08:00
KenanZhu c5e589f3d1 fix(ALConfigWidget): optimize the logic when delete user list item 2025-11-29 19:52:22 +08:00
KenanZhu 5e5deba773 fix(LibReserve): fix the mistakely passed parameter 'reserve_info'
we forget to pass the username because the
'reserve_info' do not contain the username
2025-11-28 15:15:39 +08:00
KenanZhu 842fb434f4 feat(AutoLib): new feature 'Auto Renew' 2025-11-28 15:03:51 +08:00
KenanZhu 6cabddf0cd fix(operators): optimized the reserve information pre-check and more readable output 2025-11-28 15:00:09 +08:00
KenanZhu 0322558339 fix(operators): the operations's result message only show in their output queue 2025-11-28 14:58:13 +08:00
KenanZhu 703ee527ae fix(LibChecker): fix the checker of check in and renew
we only check the reservations and their status in
today's record, and return the checked renewable
record for the upcoming new feature 'Auto-Renew'
2025-11-28 14:54:37 +08:00
KenanZhu 9a925fecb6 fix(operators): fix some type hint, and add imports for LibRenew 2025-11-28 14:53:08 +08:00
40 changed files with 4755 additions and 958 deletions
+225
View File
@@ -0,0 +1,225 @@
name: Build
# This workflow compiles the application for Windows platform using PyInstaller, and
# archives the built artifacts as 'AutoLibrary.<tag_name>-windows-x86_64.zip'.
#
# It is triggered when called by the release workflow.
on:
workflow_call:
inputs:
version:
description: 'Version number'
required: true
type: string
tag_name:
description: 'Tag name'
required: true
type: string
outputs:
version:
description: 'The version number'
value: ${{ jobs.build-windows.outputs.version }}
tag_name:
description: 'The tag name'
value: ${{ jobs.build-windows.outputs.tag_name }}
#
# Build Windows
#
jobs:
build-windows:
runs-on: windows-latest
outputs:
version: ${{ steps.get_version.outputs.VERSION }}
tag_name: ${{ steps.get_version.outputs.TAG_NAME }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: main
# here we download the build version of ALVersionInfo.py from artifacts
# and replace the committed version
- name: Download build version of ALVersionInfo.py
uses: actions/download-artifact@v4
with:
name: updated-version-info-for-build
path: src/gui/
- name: Get version info
id: get_version
run: |
$version = "${{ inputs.version }}"
$tagName = "${{ inputs.tag_name }}"
echo "TAG_NAME=$tagName" >> $env:GITHUB_OUTPUT
echo "VERSION=$version" >> $env:GITHUB_OUTPUT
Write-Host "✓ Tag: $tagName"
Write-Host "✓ Version: $version"
shell: pwsh
- name: Verify 'ALVersionInfo.py' was updated
run: |
$versionInfoFile = "src/gui/ALVersionInfo.py"
Write-Host "Verifying $versionInfoFile content:"
Write-Host "=================================="
Get-Content $versionInfoFile | Write-Host
Write-Host "=================================="
shell: pwsh
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.13'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirement.txt
- name: Solve ddddocr compatibility and copy model files
run: |
$ddddocrPath = python -c "import ddddocr, os; print(os.path.dirname(ddddocr.__file__))"
Write-Host "ddddocr package location: $ddddocrPath"
$initFile = Join-Path $ddddocrPath "__init__.py"
if (Test-Path $initFile) {
Write-Host "Fixing ddddocr compatibility in: $initFile"
(Get-Content $initFile) -replace 'Image\.ANTIALIAS', 'Image.Resampling.LANCZOS' | Set-Content $initFile
Write-Host "✓ Fixed: Image.ANTIALIAS -> Image.Resampling.LANCZOS"
} else {
Write-Error "✗ ddddocr __init__.py not found"
exit 1
}
if (-not (Test-Path "model")) {
New-Item -ItemType Directory -Path "model" | Out-Null
Write-Host "✓ Created model directory"
}
$onnxSource = Join-Path $ddddocrPath "common.onnx"
$onnxDest = "model/common.onnx"
if (Test-Path $onnxSource) {
Copy-Item $onnxSource $onnxDest -Force
Write-Host "✓ Copied ONNX model from: $onnxSource"
Write-Host "✓ ONNX model copied to: $onnxDest"
} else {
Write-Error "✗ ONNX model not found in ddddocr package: $onnxSource"
exit 1
}
if (Test-Path $onnxDest) {
$fileSize = (Get-Item $onnxDest).Length / 1MB
Write-Host "✓ Model file verified: $onnxDest (Size: $([math]::Round($fileSize, 2)) MB)"
} else {
Write-Error "✗ Failed to copy model file"
exit 1
}
shell: pwsh
- name: Compile Qt UI files
run: |
cd src/gui/batchs
./compile_ui.bat
shell: cmd
- name: Compile Qt Resource files
run: |
cd src/gui/batchs
./compile_rc.bat
shell: cmd
- name: Generate 'Main.spec'
run: |
$version = "${{ steps.get_version.outputs.VERSION }}"
$exeName = "AutoLibrary-$version"
Write-Host "Generating Main.spec for version: $version"
Write-Host "Executable name: $exeName"
$specLines = @(
"# -*- mode: python ; coding: utf-8 -*-"
""
""
"a = Analysis("
" ['src\\Main.py'],"
" pathex=[],"
" binaries=[],"
" datas=["
" ('model\\common.onnx', 'ddddocr'),"
" ('src\\gui\\icons\\AutoLibrary_32x32.ico', 'gui\\icons'),"
" ],"
" hiddenimports=[],"
" hookspath=[],"
" hooksconfig={},"
" runtime_hooks=[],"
" excludes=[],"
" noarchive=False,"
" optimize=0,"
")"
"pyz = PYZ(a.pure)"
""
"exe = EXE("
" pyz,"
" a.scripts,"
" a.binaries,"
" a.datas,"
" [],"
" name='$exeName',"
" debug=False,"
" bootloader_ignore_signals=False,"
" strip=False,"
" upx=True,"
" upx_exclude=[],"
" runtime_tmpdir=None,"
" console=False,"
" disable_windowed_traceback=False,"
" argv_emulation=False,"
" target_arch=None,"
" codesign_identity=None,"
" entitlements_file=None,"
" icon=['src\\gui\\icons\\AutoLibrary_32x32.ico'],"
")"
)
$specLines | Out-File -FilePath "Main.spec" -Encoding UTF8
Write-Host "✓ Main.spec generated successfully"
Write-Host "`nGenerated Main.spec ============"
Get-Content "Main.spec" | Write-Host
Write-Host "==================================`n"
shell: pwsh
- name: Build with PyInstaller
run: |
pyinstaller Main.spec
- name: Zip windows release
id: zip_release
run: |
$tagName = "${{ steps.get_version.outputs.TAG_NAME }}"
$version = "${{ steps.get_version.outputs.VERSION }}"
$exeName = "AutoLibrary-$version.exe"
$zipName = "AutoLibrary.$tagName-windows-x86_64.zip"
echo "ZIP_PATH=$zipName" >> $env:GITHUB_OUTPUT
Write-Host "Looking for executable: dist/$exeName"
if (Test-Path "dist/$exeName") {
Compress-Archive -Path "dist/$exeName" -DestinationPath $zipName
Write-Host "✓ Created release archive: $zipName"
} else {
Write-Error "✗ Executable not found: dist/$exeName"
Write-Host "Files in dist directory:"
Get-ChildItem "dist" | ForEach-Object { Write-Host " - $($_.Name)" }
exit 1
}
shell: pwsh
- name: Archive artifacts
uses: actions/upload-artifact@v4
with:
name: AutoLibrary.${{ steps.get_version.outputs.TAG_NAME }}-windows-x86_64
path: |
${{ steps.zip_release.outputs.ZIP_PATH }}
+109
View File
@@ -0,0 +1,109 @@
name: Commit Release
# This workflow commits version changes in 'ALVersionInfo.py' (get from artifacts) and
# moves the release tag to this new release commit.
#
# It is triggered when called by the release workflow.
on:
workflow_call:
inputs:
tag_name:
description: 'Tag name to move (e.g., v1.0.0)'
required: true
type: string
version:
description: 'Version number for commit message'
required: true
type: string
file_path:
description: 'File path to commit'
required: true
type: string
outputs:
new_commit_sha:
description: 'The new commit SHA after moving the tag'
value: ${{ jobs.commit-release.outputs.new_commit_sha }}
jobs:
commit-release:
runs-on: ubuntu-latest
permissions:
contents: write
outputs:
new_commit_sha: ${{ steps.commit_info.outputs.commit_sha }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: main
fetch-depth: 0
# here we download the commit version of ALVersionInfo.py from artifacts
# and replace the original file with it.
- name: Download commit version of ALVersionInfo.py
uses: actions/download-artifact@v4
with:
name: updated-version-info-for-commit
path: downloaded-file/
- name: Replace file with updated version
run: |
FILE_PATH="${{ inputs.file_path }}"
FILE_NAME=$(basename "$FILE_PATH")
TARGET_DIR=$(dirname "$FILE_PATH")
mkdir -p "$TARGET_DIR"
cp "downloaded-file/$FILE_NAME" "$FILE_PATH"
echo "✓ File replaced: $FILE_PATH"
echo ""
echo "Updated file content ==================="
cat "$FILE_PATH"
echo "========================================"
- name: Commit changes
id: commit_changes
run: |
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
FILE_PATH="${{ inputs.file_path }}"
VERSION="${{ inputs.version }}"
if [ ! -f "$FILE_PATH" ]; then
echo "✗ Error: File $FILE_PATH not found"
exit 1
fi
git add "$FILE_PATH"
git commit -m "chore(release): v${VERSION} [auto release commit]"
echo "✓ Changes committed"
- name: Push to main branch
run: |
MAIN_BRANCH=$(git remote show origin | grep 'HEAD branch' | cut -d' ' -f5)
if [ -z "$MAIN_BRANCH" ]; then
MAIN_BRANCH="main"
fi
echo "Pushing to branch: ${MAIN_BRANCH}"
git push origin HEAD:${MAIN_BRANCH}
echo "✓ Changes pushed to ${MAIN_BRANCH}"
- name: Move tag to new release commit
run: |
TAG_NAME="${{ inputs.tag_name }}"
echo "Moving tag ${TAG_NAME} to the new commit..."
git tag -f ${TAG_NAME}
git push origin ${TAG_NAME} --force
echo "✓ Tag ${TAG_NAME} moved to commit $(git rev-parse --short HEAD)"
- name: Output commit info
id: commit_info
run: |
COMMIT_SHA=$(git rev-parse --short HEAD)
echo "commit_sha=$COMMIT_SHA" >> $GITHUB_OUTPUT
echo "✓ New commit SHA: $COMMIT_SHA"
+156
View File
@@ -0,0 +1,156 @@
name: Release
# This workflow automates the complete release process for AutoLibrary application
# It is triggered when a new version tag (vX.Y.Z) is pushed to the repository
#
# Workflow Steps:
# START >
# 1. Update Version:
# Updates version information in 'ALVersionInfo.py' with build metadata and archives
# the updated version file as an artifact.
#
# for more information, please refer to the comment in the workflow 'update-version.yml'
# 2. Commit Release:
# Commits version changes and moves the release tag to this new release commit.
# 3. Build:
# Compiles the application for Windows platform using PyInstaller, and
# archives the built artifacts as 'AutoLibrary.<tag_name>-windows-x86_64.zip'.
# 4. Release:
# Creates GitHub release with generated artifacts and release notes
# < END
#
# The workflow ensures version consistency between source code, built artifacts, and GitHub releases
# while maintaining proper commit history and tag management.
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
jobs:
#
# Start :
# virtual job that indacates the start of the release process
#
start:
runs-on: ubuntu-latest
steps:
- name: Start release
run: |
echo "✓ Starting release"
#
# Update version :
# this job updates the version in the file 'ALVersionInfo.py'
#
update-version:
needs:
- start
uses: ./.github/workflows/update-version.yml
permissions:
contents: write
with:
tag_name: ${{ github.ref_name }}
ref: ${{ github.ref }}
#
# Commit release :
# this job commits the updated version file and move the release
# tag to this new commit
#
commit-release:
needs:
- update-version
if: ${{ needs.update-version.outputs.has_changes == 'true' }}
uses: ./.github/workflows/commit-release.yml
permissions:
contents: write
with:
tag_name: ${{ needs.update-version.outputs.tag_name }}
version: ${{ needs.update-version.outputs.version }}
file_path: src/gui/ALVersionInfo.py
#
# Build :
# this job builds the application artifacts and archives them
build:
needs:
- update-version
- commit-release
if: always() && needs.update-version.result == 'success' && needs.commit-release.result == 'success'
uses: ./.github/workflows/build.yml
permissions:
contents: write
with:
version: ${{ needs.update-version.outputs.version }}
tag_name: ${{ needs.update-version.outputs.tag_name }}
#
# Release :
# this job creates a GitHub release and uploads the archive files
release:
runs-on: ubuntu-latest
needs:
- build
if: always() && needs.build.result == 'success'
permissions:
contents: write
steps:
- name: Download artifacts
uses: actions/download-artifact@v4
with:
name: AutoLibrary.${{ needs.build.outputs.tag_name }}-windows-x86_64
path: artifacts/
- name: Create release
id: create_release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.build.outputs.tag_name }}
name: AutoLibrary ${{ needs.build.outputs.tag_name }}
files: |
artifacts/AutoLibrary.${{ needs.build.outputs.tag_name }}-windows-x86_64.zip
draft: false
prerelease: false
generate_release_notes: true
body: |
### 下载获取
- **Windows x86_64**: `AutoLibrary.${{ needs.build.outputs.tag_name }}-windows-x86_64.zip`
### 如何使用
1. 下载 `AutoLibrary.${{ needs.build.outputs.tag_name }}-windows-x86_64.zip` 文件
2. 解压到任意目录
3. 下载对应浏览器的驱动文件
4. 运行 `AutoLibrary-${{ needs.build.outputs.version }}.exe` (首次运行会初始化配置文件)
5. 按照提示操作即可
更多详情请访问 [AutoLibrary 网站](http://autolibrary.cv) 和查看 [帮助手册](https://autolibrary.cv/docs/manual_lists.html)
---
**完整更新日志见下方自动生成的 Release Notes**
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# End :
# virtual job that indacates the end of the release process
#
end:
needs:
- release
runs-on: ubuntu-latest
steps:
- name: End release
run: |
echo "✓ Ending release"
+163
View File
@@ -0,0 +1,163 @@
name: Update Version
# This workflow updates version information in 'ALVersionInfo.py' with build metadata.
# In progress, it will generate two version files, the first one is locate in 'src/gui/ALVersionInfo.py',
# and the second one is locate in 'src/gui/temp/ALVersionInfo.py'. The first one is use
# in the release process, it only update the version and tag name. The commit and build infomation
# is 'local' or 'null'. All of them will finally archive as artifacts.
#
# It is triggered when called by the release workflow.
on:
workflow_call:
inputs:
tag_name:
description: 'Tag name'
required: true
type: string
ref:
description: 'Git ref to checkout'
required: true
type: string
outputs:
tag_name:
description: 'The tag name'
value: ${{ jobs.update-version.outputs.tag_name }}
version:
description: 'The version number'
value: ${{ jobs.update-version.outputs.version }}
has_changes:
description: 'Whether ALVersionInfo.py was modified'
value: ${{ jobs.update-version.outputs.has_changes }}
jobs:
update-version:
runs-on: ubuntu-latest
permissions:
contents: write
outputs:
tag_name: ${{ steps.get_version.outputs.TAG_NAME }}
version: ${{ steps.get_version.outputs.VERSION }}
has_changes: ${{ steps.check_changes.outputs.has_changes }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref }}
fetch-depth: 0
- name: Get tag name and version
id: get_version
env:
TZ: UTC
run: |
TAG_NAME="${{ inputs.tag_name }}"
VERSION="${TAG_NAME#v}"
COMMIT_SHA="${GITHUB_SHA:0:7}"
COMMIT_DATE=$(TZ=UTC git log -1 --format=%cd --date=format-local:'%Y-%m-%d %H:%M:%S UTC')
echo "TAG_NAME=$TAG_NAME" >> $GITHUB_OUTPUT
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
echo "COMMIT_SHA=$COMMIT_SHA" >> $GITHUB_OUTPUT
echo "COMMIT_DATE=$COMMIT_DATE" >> $GITHUB_OUTPUT
echo "✓ Tag: $TAG_NAME"
echo "✓ Version: $VERSION"
echo "✓ Commit SHA: $COMMIT_SHA"
echo "✓ Commit Date: $COMMIT_DATE"
- name: Create 'temp' directory
run: |
echo "Creating temp directory..."
mkdir -p "src/gui/temp"
echo "✓ temp directory created successfully"
- name: Update ALVersionInfo.py with version info
run: |
VERSION="${{ steps.get_version.outputs.VERSION }}"
TAG_NAME="${{ steps.get_version.outputs.TAG_NAME }}"
COMMIT_SHA="${{ steps.get_version.outputs.COMMIT_SHA }}"
COMMIT_DATE="${{ steps.get_version.outputs.COMMIT_DATE }}"
VER_INFO_BUILDFILE="src/gui/temp/ALVersionInfo.py"
VER_INFO_COMMITFILE="src/gui/ALVersionInfo.py"
BUILD_DATE=$(date -u '+%Y-%m-%d %H:%M:%S UTC')
echo "Updating ALVersionInfo.py files with build information..."
{
echo '# -*- coding: utf-8 -*-'
echo ''
echo '"""'
echo ' The contents of this file will automatically be updated by the'
echo ' workflow process. Do not edit manually.'
echo ''
echo ' This file is auto-generated during the workflow process.'
echo " Last updated: ${BUILD_DATE}"
echo '"""'
echo ''
echo "AL_VERSION = \"${VERSION}\""
echo "AL_TAG = \"${TAG_NAME}\""
echo "AL_COMMIT_SHA = \"${COMMIT_SHA}\""
echo "AL_COMMIT_DATE = \"${COMMIT_DATE}\" # time zone : UTC"
echo "AL_BUILD_DATE = \"${BUILD_DATE}\" # time zone : UTC"
echo 'AL_VERSION_FULL = f"{AL_VERSION} ({AL_COMMIT_SHA})"'
} > "$VER_INFO_BUILDFILE"
echo "Updating ALVersionInfo.py for release commit..."
{
echo '# -*- coding: utf-8 -*-'
echo ''
echo '"""'
echo ' The contents of this file will automatically be updated by the'
echo ' workflow process. Do not edit manually.'
echo ''
echo ' This file is auto-generated during the workflow process.'
echo " Last updated: ${BUILD_DATE}"
echo '"""'
echo ''
echo "AL_VERSION = \"${VERSION}\""
echo "AL_TAG = \"${TAG_NAME}\""
echo "AL_COMMIT_SHA = \"local\""
echo "AL_COMMIT_DATE = \"null\" # time zone : UTC"
echo "AL_BUILD_DATE = \"null\" # time zone : UTC"
echo 'AL_VERSION_FULL = f"{AL_VERSION} ({AL_COMMIT_SHA})"'
} > "$VER_INFO_COMMITFILE"
echo "✓ ALVersionInfo.py files updated successfully"
echo ""
echo "Build version file location: $VER_INFO_BUILDFILE"
echo "Commit version file location: $VER_INFO_COMMITFILE"
echo ""
echo "Build version ALVersionInfo.py content ="
cat "$VER_INFO_BUILDFILE"
echo ""
echo "Commit version ALVersionInfo.py content "
cat "$VER_INFO_COMMITFILE"
echo "========================================"
- name: Check if ALVersionInfo.py was modified
id: check_changes
run: |
if git diff --quiet src/gui/ALVersionInfo.py; then
echo "has_changes=false" >> $GITHUB_OUTPUT
echo "! No changes detected in ALVersionInfo.py"
else
echo "has_changes=true" >> $GITHUB_OUTPUT
echo "✓ ALVersionInfo.py has been modified"
fi
- name: Upload modified ALVersionInfo.py ready for build
if: steps.check_changes.outputs.has_changes == 'true'
uses: actions/upload-artifact@v4
with:
name: updated-version-info-for-build
path: src/gui/temp/ALVersionInfo.py
retention-days: 1
- name: Upload modified ALVersionInfo.py ready for commit
if: steps.check_changes.outputs.has_changes == 'true'
uses: actions/upload-artifact@v4
with:
name: updated-version-info-for-commit
path: src/gui/ALVersionInfo.py
retention-days: 1
+4
View File
@@ -14,4 +14,8 @@ src/gui/AutoLibraryResources.py
src/gui/AutoLibraryResource.py
src/gui/Ui_ALMainWindow.py
src/gui/Ui_ALConfigWidget.py
src/gui/Ui_ALTimerTaskWidget.py
src/gui/Ui_ALAddTimerTaskDialog.py
src/gui/Ui_ALAboutDialog.py
Main.spec
+115 -2
View File
@@ -1,5 +1,118 @@
# AutoLibrary
---
请访问[AutoLibrary 网站](http://autolibrary.cv)\
Please access the [AutoLibrary Website](http://autolibrary.cv)
![AutoLibrary Logo](./src/gui/icons/AutoLibrary_128x128.ico)
![License](https://img.shields.io/github/license/KenanZhu/AutoLibrary)
![Issue](https://img.shields.io/github/issues/KenanZhu/AutoLibrary)
![Release](https://img.shields.io/github/v/release/KenanZhu/AutoLibrary)
![Download](https://img.shields.io/github/downloads/KenanZhu/AutoLibrary/total)
了解更多请访问 [_AutoLibrary 网站_](http://autolibrary.cv)
---
### 功能
1. 自动预约 - 支持自动预约
2. 自动续约 - 支持自动续约
3. 自动签到 - 支持自动签到
4. 批量操作 - 支持同时预约多个用户,可以指定当前需要跳过的用户,并将用户分成多个组
5. 定时任务 - 使用内置定时任务管理,添加定时任务,指定时间后按当前预约信息自动运行
*1,2,3 的具体操作方法和注意事项请访问我们的 [帮助手册](https://autolibrary.cv/docs/manual_lists.html)*
### 特点
#### 关于预约等操作的注意事项
工具会自动处理登录过程的验证码识别过程,正常情况下单次识别准确率在 90% 以上,如遇验证码识别错误,大概率是校园网网络环境不佳导致的。
只要确保处于校园网网络环境内,工具都是可以正常运行的。操作处理速度基本上取决于校园网的网络环境,一般情况下在 3-4 秒(不考虑硬件差异)左右即可完成一个用户的操作,完全满足正常使用需求。
> [!NOTE]
> 工具仅作为正常的预约,签到和续约的图书馆辅助工具,请勿干扰图书馆的正常运行(如故意预约多个座位,或同时预约大量的用户等,对此影响图书馆正常运行本工具概不负责,请在善用工具方便自己的情况下尽量不用影响其他同学的使用)。
#### 关于批量操作的注意事项
批量操作时,建议将需要操作的用户分成多个组,每个组的用户数量不要超过 4 人(即一整张桌子的数量),否则会影响操作效率,大量用户同时预约会一定程度上增加图书馆服务器的压力,影响正常使用。根据需要在用户管理界面中可以勾选本次操作是否跳过该用户,以提高运行效率。
#### 关于定时任务的注意事项
定时任务会在指定的时间自动运行,运行时会根据当前预约信息进行操作。一般情况下不建议设置两个运行开始时间比较接近的定时任务,否则后一个任务会等待前一个任务完成后才会运行,按照队列的顺序执行。
### 如何使用
1. 下载最新版本的 [AutoLibrary 压缩包](https://github.com/KenanZhu/AutoLibrary/releases)。
2. 解压下载的文件到任意目录。
3. 下载对应浏览器的驱动文件,并在配置界面的运行配置选项卡对应位置选择你下载好的浏览器驱动
4. 运行 `AutoLibrary.exe` 文件。
5. 按照提示操作即可。
*注意 1*: 关于浏览器驱动的下载和其它相关问题,请参考我们的 [帮助手册](https://autolibrary.cv/docs/manual_lists.html) 中对应软件版本的内容。
#### 平台支持 & 编译步骤
本工具目前仅支持 Windows 平台,由于使用 PySide6 库开发,理论上是可以自行编译并在 Linux 和 macOS 上运行,这里提供简单的编译步骤:
1. 确保系统安装了 Python 3.13 版本 (推荐,过低或高版本会导致兼容问题)。
2. 安装 pyside6 selenium ddddocr 库,命令为 `pip install pyside6 selenium ddddocr`
3.`src/gui/batchs` 目录下运行 `compile_ui.bat` linux 和 macOS 系统使用 `compile_ui.sh`) 文件来编译 Qt 的 UI 文件。
4. 在上一步相同目录内运行 `compile_rc.bat` linux 和 macOS 系统使用 `compile_rc.sh` 文件来编译 Qt 的资源文件。
5. 待上述步骤完成后,运行 `src/Main.py` 文件即可。
*注意 1*:如果 python 使用的是虚拟环境,请在虚拟环境安装依赖后,在激活的虚拟环境终端中使用 `cd src/gui/batchs` 命令切换到 `batchs` 目录下,再运行编译脚本。否则会提示缺少必要的 Qt PySide 依赖库。
*注意 2*:由于 ddddocr 的代码版本问题,其中 `__init__.py` 文件中的函数 `def classification(self, img: bytes):` 中的 `image.resize` 方法传入了不符合当前 pillow 版本的 `resample` 参数 `Image.ANTIALIAS`,该重采样常量已经在 10.0.0 版中删除 [1](@ref)。请将 `image.resize` 方法中的参数替换为 `resample=Image.Resampling.LANCZOS`,具体函数如下:
```python
def classification(self, img: bytes):
image = Image.open(io.BytesIO(img))
image = image.resize((int(image.size[0]*(64/image.size[1])), 64), Image.ANTIALIAS).convert('L')
^^^^^
请将上述参数替换为 `Image.Resampling.LANCZOS`
...
```
[1](@ref)[pillow 中已经删除或已经弃用的常量](https://pillow.ac.cn/en/stable/deprecations.html#constants)
### Q&A
#### 为什么开发这个工具?
当前图书馆的座位预约系统在使用中确实会遇到一些不便。例如,系统登录界面较为陈旧,在输入验证码时,若出现错误常常需要全部重新填写,过程繁琐。尤其在网络环境不稳定的情况下,登录和加载速度缓慢,让人难以快速完成当天的签到或预约次日座位。
此外,当朋友需要帮忙预约座位时,手动操作也会分散自己学习和工作的注意力。
因此,很希望有一个便捷的工具能自动处理这些预约、续约和签到等操作,从而让自己从这些琐碎事务中解脱出来,更专注于手头的重要事项。
#### 工具后续会收费吗?
不会,本工具完全免费使用,也不会有任何额外收费项。如果你觉工具对你很有帮助,可以为我捐助一瓶饮料的价格,以用于 AutoLibrary 网站的维护和软件的稳定更新。
<a href="https://afdian.com/a/autolibrary" style="display:inline-block;padding:10px 30px;background:linear-gradient(135deg,#946CE6,#946CE6);color:white;text-decoration:none;border-radius:6px;font-weight:bold;">❤ 支持作者</a>
#### 会有手机端的版本吗?
暂时没有考虑,而且也没有足够的时间和能力开发多平台的版本并测试维护,所以暂时只提供 Windows 版本。
#### 后续会有哪些功能?
当前 v1.0.0 版本的功能对于正常使用已经足够,不过后续会着重考虑完善 2-4 人预约时的使用体验,暂时有以下构想:
1. 2-4 人一起预约时,往往会偏向于预约并排或对面的整个空座位,这时候工具会按照一定策略查询搜索符合条件的座位,并预约并排或对面的整个座位,而不是各自独立预约。
2. 预约时会考虑到组内用户的预约时间是否冲突,若冲突则会提示用户是否继续预约,若用户选择继续预约,则会按需要调整预约时间,再进行预约。
3. 对于比较固定的用户,会考虑在定时任务管理中添加如 ‘每日任务’ ‘每周任务’ 等选项,用户可以根据需要设置定时任务重复的日期范围,自动完成预约,签到,续约等操作。
不过由于本人的时间和能力有限,也需要考虑到图书馆的正常运行,所以后续功能会有所取舍,但也许会进行一些小的功能验证。
#### 其他功能建议?
如果你有其他功能建议,或者遇到了什么功能性,操作上的问题,欢迎提交 Issue 到本项目。
如果你有足够的开发能力,欢迎为本项目提交 PR,也可以 Fork 本项目,根据自己的需求进行修改和完善。
### 联系我
- 项目维护:[KenanZhu (Nanoki)](https://github.com/KenanZhu)
- 电子邮箱:<nanoki_zh@163.com>
_**Free to use** —— AutoLibrary 是一个基于 MIT 协议免费开源的工具_
+2 -2
View File
@@ -29,7 +29,7 @@ class MsgBase:
msg: str
):
self._output_queue.put(f"[{self._class_name:<12}] >>> : {msg}")
self._output_queue.put(f"[{self._class_name:<15}] >>> : {msg}")
def _showTrace(
@@ -38,7 +38,7 @@ class MsgBase:
):
timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
self._output_queue.put(f"{timestamp}-[{self._class_name:<12}] : {msg}")
self._output_queue.put(f"{timestamp}-[{self._class_name:<15}] : {msg}")
def _waitMsg(
+146
View File
@@ -0,0 +1,146 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2025 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details.
"""
import sys
import platform
from PySide6.QtGui import (
QIcon
)
from PySide6.QtWidgets import (
QDialog, QApplication
)
from PySide6.QtCore import (
QTimer, Qt
)
from gui.ALVersionInfo import (
AL_VERSION, AL_COMMIT_SHA, AL_COMMIT_DATE, AL_BUILD_DATE
)
from gui.Ui_ALAboutDialog import Ui_ALAboutDialog
from gui import AutoLibraryResource
class ALAboutDialog(QDialog, Ui_ALAboutDialog):
def __init__(
self,
parent = None
):
super().__init__(parent)
self.setupUi(self)
self.modifyUi()
self.connectSignals()
def modifyUi(
self
):
self.LogoIconLabel.setPixmap(QIcon(":/res/icon/icons/AutoLibrary_32x32.ico").pixmap(48, 48))
info_text = self.generateAboutText()
self.AboutInfoEdit.setHtml(info_text)
self.AboutInfoEdit.setTextInteractionFlags(Qt.TextBrowserInteraction)
def connectSignals(
self
):
self.CopyButton.clicked.connect(self.copyAboutInfo)
def generateAboutText(
self
):
os_info = self.getOSInfo()
about_text = f"""
<h4>Version Information:</h4>
Version: {AL_VERSION}<br>
Commit SHA: {AL_COMMIT_SHA}<br>
Commit date: {AL_COMMIT_DATE}<br>
Build date: {AL_BUILD_DATE}<br>
Python version: {platform.python_version()}<br>
Qt version: {self.getQtVersion()}<br>
<h4>System Information:</h4>
Processor: {platform.processor()}<br>
Operating system: {os_info['system']}<br>
System version: {os_info['version']}<br>
System architecture: {os_info['architecture']}<br>
<h4>Project Information:</h4>
License: MIT License<br>
Project repository: <a href="https://www.github.com/KenanZhu/AutoLibrary" style="text-decoration: none;">https://www.github.com/KenanZhu/AutoLibrary</a><br>
Project website: <a href="https://www.autolibrary.cv/" style="text-decoration: none;">https://www.autolibrary.cv/</a><br>
<h4>Author Information:</h4>
Developer: KenanZhu<br>
Contact: nanoki_zh@163.com<br>
GitHub: <a href="https://www.github.com/KenanZhu" style="text-decoration: none;">https://www.github.com/KenanZhu</a><br>
"""
return about_text
def getOSInfo(
self
):
system = platform.system()
version = platform.version()
architecture = platform.architecture()[0]
if system == "Windows":
try:
version = platform.win32_ver()[1]
except:
pass
elif system == "Darwin":
try:
version = platform.mac_ver()[0]
except:
pass
elif system == "Linux":
try:
import distro # try to get Linux distro info
version = f"{distro.name()} {distro.version()}"
except ImportError:
pass
return {
'system': system,
'version': version,
'architecture': architecture
}
def getQtVersion(
self
):
try:
from PySide6.QtCore import qVersion
return qVersion()
except:
return "Unknown"
def copyAboutInfo(
self
):
about_text = self.AboutInfoEdit.toPlainText()
clipboard = QApplication.clipboard()
clipboard.setText(about_text)
original_text = self.CopyButton.text()
self.CopyButton.setText("已复制")
QTimer.singleShot(2000, lambda: self.CopyButton.setText(original_text))
+175
View File
@@ -0,0 +1,175 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ALAboutDialog</class>
<widget class="QDialog" name="ALAboutDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>400</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>400</width>
<height>400</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>800</width>
<height>400</height>
</size>
</property>
<property name="windowTitle">
<string>关于 - AutoLibrary</string>
</property>
<property name="sizeGripEnabled">
<bool>false</bool>
</property>
<layout class="QVBoxLayout" name="ALAboutDialogLayout">
<property name="spacing">
<number>5</number>
</property>
<property name="leftMargin">
<number>5</number>
</property>
<property name="topMargin">
<number>5</number>
</property>
<property name="rightMargin">
<number>5</number>
</property>
<property name="bottomMargin">
<number>5</number>
</property>
<item>
<layout class="QHBoxLayout" name="LogoLayout">
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="QLabel" name="LogoIconLabel">
<property name="minimumSize">
<size>
<width>56</width>
<height>56</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>56</width>
<height>56</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="scaledContents">
<bool>true</bool>
</property>
<property name="indent">
<number>0</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="LogoTextLabel">
<property name="font">
<font>
<pointsize>24</pointsize>
<bold>true</bold>
</font>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<property name="text">
<string>AutoLibrary</string>
</property>
<property name="margin">
<number>0</number>
</property>
<property name="indent">
<number>0</number>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="AboutInfoLayout">
<property name="spacing">
<number>0</number>
</property>
<item>
<widget class="QFrame" name="frame">
<property name="minimumSize">
<size>
<width>56</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>56</width>
<height>16777215</height>
</size>
</property>
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Plain</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
</widget>
</item>
<item>
<widget class="QTextEdit" name="AboutInfoEdit">
<property name="font">
<font>
<family>Courier New</family>
<bold>false</bold>
</font>
</property>
<property name="lineWrapMode">
<enum>QTextEdit::LineWrapMode::NoWrap</enum>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::TextInteractionFlag::TextBrowserInteraction</set>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QPushButton" name="CopyButton">
<property name="minimumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>复制</string>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
+150
View File
@@ -0,0 +1,150 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2025 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details.
"""
import os
import sys
import time
import uuid
import queue
from enum import Enum
from datetime import datetime, timedelta
from PySide6.QtCore import (
Qt, Signal, Slot, QDateTime
)
from PySide6.QtWidgets import (
QLabel, QDialog, QWidget, QSpinBox, QVBoxLayout,
QHBoxLayout, QGridLayout, QDateTimeEdit
)
from PySide6.QtGui import (
QCloseEvent
)
from gui.Ui_ALAddTimerTaskDialog import Ui_ALAddTimerTaskDialog
class TimerTaskStatus(Enum):
PENDING = "等待中"
READY = "已就绪"
RUNNING = "执行中"
EXECUTED = "已执行"
ERROR = "执行失败"
OUTDATED = "已过期"
class ALAddTimerTaskWidget(QDialog, Ui_ALAddTimerTaskDialog):
def __init__(
self,
parent = None
):
super().__init__(parent)
self.setupUi(self)
self.connectSignals()
self.modifyUi()
def modifyUi(
self
):
self.TimerTypeComboBox.setCurrentIndex(0)
self.SpecificTimerWidget = QWidget()
self.SpecificTimerLayout = QHBoxLayout(self.SpecificTimerWidget)
self.SpecificTimerLayout.addWidget(QLabel("定时时间:"))
self.SpecificDateTimeEdit = QDateTimeEdit()
self.SpecificDateTimeEdit.setCalendarPopup(True)
self.SpecificDateTimeEdit.setDisplayFormat("yyyy-MM-dd HH:mm:ss")
self.SpecificDateTimeEdit.setMinimumDateTime(QDateTime.currentDateTime())
self.SpecificDateTimeEdit.setDateTime(QDateTime.currentDateTime().addSecs(60))
self.SpecificTimerLayout.addWidget(self.SpecificDateTimeEdit)
self.TimerConfigLayout.addWidget(self.SpecificTimerWidget)
self.RelativeTimerWidget = QWidget()
self.RelativeTimerLayout = QGridLayout(self.RelativeTimerWidget)
self.RelativeTimerLayout.addWidget(QLabel("相对时间:"), 0, 0)
self.RelativeDaySpinBox = QSpinBox()
self.RelativeDaySpinBox.setMinimum(0)
self.RelativeDaySpinBox.setMaximum(365)
self.RelativeDaySpinBox.setSuffix("")
self.RelativeTimerLayout.addWidget(self.RelativeDaySpinBox, 1, 0)
self.RelativeHourSpinBox = QSpinBox()
self.RelativeHourSpinBox.setMinimum(0)
self.RelativeHourSpinBox.setMaximum(23)
self.RelativeHourSpinBox.setSuffix("")
self.RelativeTimerLayout.addWidget(self.RelativeHourSpinBox, 1, 1)
self.RelativeMinuteSpinBox = QSpinBox()
self.RelativeMinuteSpinBox.setMinimum(0)
self.RelativeMinuteSpinBox.setMaximum(59)
self.RelativeMinuteSpinBox.setSuffix("")
self.RelativeTimerLayout.addWidget(self.RelativeMinuteSpinBox, 1, 2)
self.RelativeSecondSpinBox = QSpinBox()
self.RelativeSecondSpinBox.setMinimum(0)
self.RelativeSecondSpinBox.setMaximum(59)
self.RelativeSecondSpinBox.setSuffix("")
self.RelativeTimerLayout.addWidget(self.RelativeSecondSpinBox, 1, 3)
self.TimerConfigLayout.addWidget(self.RelativeTimerWidget)
self.RelativeTimerWidget.setVisible(False)
def connectSignals(
self
):
self.CancelButton.clicked.connect(self.reject)
self.ConfirmButton.clicked.connect(self.accept)
self.TimerTypeComboBox.currentIndexChanged.connect(self.onTimerTypeComboBoxIndexChanged)
def getTimerTask(
self
) -> dict:
added_time = datetime.now()
if not self.TaskNameLineEdit.text():
name = f"未命名任务-{added_time.strftime("%Y%m%d%H%M%S")}"
else:
name = self.TaskNameLineEdit.text()
timer_type_index = self.TimerTypeComboBox.currentIndex()
silent = not self.ShowBeforeRunRadioButton.isChecked()
if timer_type_index == 0:
execute_time = self.SpecificDateTimeEdit.dateTime()
tmp_time_str = execute_time.toString("yyyy-MM-dd HH:mm:ss")
execute_time = datetime.strptime(tmp_time_str, "%Y-%m-%d %H:%M:%S")
else:
execute_time = datetime.now() + timedelta(
days = self.RelativeDaySpinBox.value(),
hours = self.RelativeHourSpinBox.value(),
minutes = self.RelativeMinuteSpinBox.value(),
seconds = self.RelativeSecondSpinBox.value()
)
return {
"name": name,
"task_uuid": uuid.uuid4().hex.upper() + f"-{added_time.strftime("%Y%m%d%H%M%S")}",
"time_type": self.TimerTypeComboBox.currentText(),
"execute_time": execute_time,
"silent": silent,
"add_time": added_time,
"status": TimerTaskStatus.PENDING,
"executed": False
}
@Slot(int)
def onTimerTypeComboBoxIndexChanged(
self,
index: int
):
self.SpecificTimerWidget.setVisible(index == 0)
self.RelativeTimerWidget.setVisible(index == 1)
+249
View File
@@ -0,0 +1,249 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ALAddTimerTaskDialog</class>
<widget class="QDialog" name="ALAddTimerTaskDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>300</width>
<height>300</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>300</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>500</width>
<height>300</height>
</size>
</property>
<property name="windowTitle">
<string>添加定时任务 - AutoLibrary</string>
</property>
<property name="sizeGripEnabled">
<bool>true</bool>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="ALAddTimerTaskLayout">
<property name="spacing">
<number>5</number>
</property>
<property name="leftMargin">
<number>5</number>
</property>
<property name="topMargin">
<number>5</number>
</property>
<property name="rightMargin">
<number>5</number>
</property>
<property name="bottomMargin">
<number>5</number>
</property>
<item>
<layout class="QHBoxLayout" name="TaskNameLayout">
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="QLabel" name="TaskNameLabel">
<property name="minimumSize">
<size>
<width>60</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>任务名称:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="TaskNameLineEdit"/>
</item>
</layout>
</item>
<item>
<widget class="QGroupBox" name="TimerConfigGroupBox">
<property name="title">
<string>定时设置</string>
</property>
<layout class="QVBoxLayout" name="TimerConfigLayout">
<property name="spacing">
<number>5</number>
</property>
<property name="leftMargin">
<number>5</number>
</property>
<property name="topMargin">
<number>5</number>
</property>
<property name="rightMargin">
<number>5</number>
</property>
<property name="bottomMargin">
<number>5</number>
</property>
<item>
<layout class="QHBoxLayout" name="TimerTypeSelectLayout">
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="QLabel" name="TimerTypeLabel">
<property name="text">
<string>定时类型:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="TimerTypeComboBox">
<item>
<property name="text">
<string>特定时间</string>
</property>
</item>
<item>
<property name="text">
<string>相对时间</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="TaskConfigGroupBox">
<property name="title">
<string>运行设置</string>
</property>
<layout class="QGridLayout" name="TaskConfigLayout">
<property name="leftMargin">
<number>5</number>
</property>
<property name="topMargin">
<number>5</number>
</property>
<property name="rightMargin">
<number>5</number>
</property>
<property name="bottomMargin">
<number>5</number>
</property>
<property name="spacing">
<number>5</number>
</property>
<item row="0" column="0">
<widget class="QRadioButton" name="SilentlyRunRadioButton">
<property name="text">
<string>静默运行</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
<property name="autoRepeat">
<bool>false</bool>
</property>
<property name="autoExclusive">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QRadioButton" name="ShowBeforeRunRadioButton">
<property name="text">
<string>运行前提示</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="ControLayout">
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="QFrame" name="ControlSpaceFrame">
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Plain</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="CancelButton">
<property name="minimumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>取消</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="ConfirmButton">
<property name="minimumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>添加</string>
</property>
<property name="autoDefault">
<bool>true</bool>
</property>
<property name="default">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
+529 -251
View File
File diff suppressed because it is too large Load Diff
+624 -454
View File
File diff suppressed because it is too large Load Diff
+230 -123
View File
@@ -13,112 +13,33 @@ import time
import queue
from PySide6.QtCore import (
Qt, Signal, Slot, QDir, QFileInfo, QTimer, QThread
Qt, Signal, Slot, QDir, QFileInfo, QTimer, QUrl,
)
from PySide6.QtWidgets import (
QMainWindow, QMenu
QMainWindow, QMenu, QSystemTrayIcon
)
from PySide6.QtGui import (
QTextCursor, QCloseEvent, QFont, QIcon
QTextCursor, QCloseEvent, QFont, QIcon, QDesktopServices
)
from .Ui_ALMainWindow import Ui_ALMainWindow
from .ALConfigWidget import ALConfigWidget
from gui.Ui_ALMainWindow import Ui_ALMainWindow
from gui.ALConfigWidget import ALConfigWidget
from gui.ALTimerTaskWidget import ALTimerTaskWidget
from gui.ALAboutDialog import ALAboutDialog
from gui.ALMainWorkers import TimerTaskWorker, AutoLibWorker
from . import AutoLibraryResource
from gui import AutoLibraryResource
from operators.AutoLib import AutoLib
from utils.ConfigReader import ConfigReader
class AutoLibWorker(QThread):
finishedSignal = Signal()
showTraceSignal = Signal(str)
showMsgSignal = Signal(str)
def __init__(
self,
input_queue: queue.Queue,
output_queue: queue.Queue,
config_paths: dict
):
super().__init__()
self.__input_queue = input_queue
self.__output_queue = output_queue
self.__config_paths = config_paths
self.__stopped = False
def checkTimeAvailable(
self,
) -> bool:
current_time = time.strftime("%H:%M", time.localtime())
if current_time >= "23:30" or current_time <= "07:30":
return False
return True
def checkConfigPaths(
self,
) -> bool:
if not all(
os.path.exists(path) for path in self.__config_paths.values()
):
self.showTraceSignal.emit(
"配置文件路径不存在, 请检查配置文件路径是否正确。"
)
return False
return True
def run(
self
):
auto_lib = None
try:
if not self.checkTimeAvailable():
self.showTraceSignal.emit(
"当前时间不在图书馆开放时间内。\n"\
" 请在 07:30 - 23:30 之间尝试"
)
return
if not self.checkConfigPaths():
return
self.showTraceSignal.emit("AutoLibrary 开始运行")
auto_lib = AutoLib(
self.__input_queue,
self.__output_queue,
)
auto_lib.run(
ConfigReader(self.__config_paths["system"]),
ConfigReader(self.__config_paths["users"]),
)
except Exception as e:
self.showTraceSignal.emit(
f"AutoLibrary 运行时发生异常 : {e}"
)
finally:
if auto_lib:
auto_lib.close()
self.showTraceSignal.emit("AutoLibrary 运行结束")
self.finishedSignal.emit()
def stop(
self
):
self.__stopped = True
from utils.ConfigWriter import ConfigWriter
class ALMainWindow(QMainWindow, Ui_ALMainWindow):
timerTaskIsRunning = Signal(dict)
timerTaskIsExecuted = Signal(dict)
timerTaskIsError = Signal(dict)
def __init__(
self
):
@@ -129,27 +50,108 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
self.setupUi(self)
self.__input_queue = queue.Queue()
self.__output_queue = queue.Queue()
self.__timer_task_queue = queue.Queue()
script_path = sys.executable
script_dir = QFileInfo(script_path).absoluteDir()
self.__config_paths = {
"system": QDir.toNativeSeparators(script_dir.absoluteFilePath("system.json")),
"users": QDir.toNativeSeparators(script_dir.absoluteFilePath("users.json")),
"run": QDir.toNativeSeparators(script_dir.absoluteFilePath("run.json")),
"user": QDir.toNativeSeparators(script_dir.absoluteFilePath("user.json")),
"timer_task": QDir.toNativeSeparators(script_dir.absoluteFilePath("timer_task.json")),
}
self.__alTimerTaskWidget = None
self.__alConfigWidget = None
self.__auto_lib_thread = None
self.__current_timer_task_thread = None
self.__is_running_timer_task = False
self.modifyUi()
self.setupTray()
self.connectSignals()
self.startMsgPolling()
self.startTimerTaskPolling()
def modifyUi(
self
):
icon = QIcon(":/res/icon/icons/AutoLibrary.ico")
self.setWindowIcon(icon)
self.icon = QIcon(":/res/icon/icons/AutoLibrary_32x32.ico")
self.setWindowIcon(self.icon)
self.MessageIOTextEdit.setFont(QFont("Courier New", 10))
self.ManualAction.triggered.connect(self.onManualActionTriggered)
self.AboutAction.triggered.connect(self.onAboutActionTriggered)
# initialize timer task widget, but not show it
self.__alTimerTaskWidget = ALTimerTaskWidget(self, self.__config_paths["timer_task"])
self.timerTaskIsRunning.connect(self.__alTimerTaskWidget.onTimerTaskIsRunning)
self.timerTaskIsExecuted.connect(self.__alTimerTaskWidget.onTimerTaskIsExecuted)
self.timerTaskIsError.connect(self.__alTimerTaskWidget.onTimerTaskIsError)
self.__alTimerTaskWidget.timerTaskIsReady.connect(self.onTimerTaskIsReady)
self.__alTimerTaskWidget.timerTaskWidgetClosed.connect(self.onTimerTaskWidgetClosed)
self.__alTimerTaskWidget.setWindowFlags(Qt.WindowType.Window|Qt.WindowType.WindowCloseButtonHint)
def onAboutActionTriggered(
self
):
about_dialog = ALAboutDialog(self)
about_dialog.exec()
def onManualActionTriggered(
self
):
url = QUrl("https://www.autolibrary.cv/docs/manual_lists.html")
QDesktopServices.openUrl(url)
def setupTray(
self
):
if not QSystemTrayIcon.isSystemTrayAvailable():
self.showTraceSignal.emit(
"系统不支持系统托盘功能, 无法创建系统托盘图标。"
)
return
self.TrayIcon = QSystemTrayIcon(self.icon, self)
self.TrayIcon.setToolTip("AutoLibrary")
self.TrayMenu = QMenu()
self.TrayMenu.addAction("显示主窗口", self.showNormal)
self.TrayMenu.addAction("显示定时窗口", self.onTimerTaskWidgetButtonClicked)
self.TrayMenu.addAction("最小化到托盘", self.hideToTray)
self.TrayMenu.addSeparator()
self.TrayMenu.addAction("退出", self.close)
self.TrayIcon.setContextMenu(self.TrayMenu)
self.TrayIcon.setContextMenu(self.TrayMenu)
self.TrayIcon.activated.connect(self.onTrayIconActivated)
self.TrayIcon.show()
def hideToTray(
self
):
self.hide()
self.TrayIcon.showMessage(
"AutoLibrary",
"\n已最小化到托盘",
QSystemTrayIcon.MessageIcon.Information,
2000
)
def onTrayIconActivated(
self,
reason: QSystemTrayIcon.ActivationReason
):
if reason == QSystemTrayIcon.DoubleClick:
self.showNormal()
def connectSignals(
@@ -157,6 +159,7 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
):
self.ConfigButton.clicked.connect(self.onConfigButtonClicked)
self.TimerTaskWidgetButton.clicked.connect(self.onTimerTaskWidgetButtonClicked)
self.StartButton.clicked.connect(self.onStartButtonClicked)
self.StopButton.clicked.connect(self.onStopButtonClicked)
self.SendButton.clicked.connect(self.onSendButtonClicked)
@@ -168,10 +171,19 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
event: QCloseEvent
):
if self.__timer and self.__timer.isActive():
self.__timer.stop()
if self.__msg_queue_timer and self.__msg_queue_timer.isActive():
self.__msg_queue_timer.stop()
if self.__timer_task_timer and self.__timer_task_timer.isActive():
self.__timer_task_timer.stop()
if self.__is_running_timer_task:
self.__current_timer_task_thread.wait(2000)
self.__current_timer_task_thread.deleteLater()
if self.__alTimerTaskWidget:
self.__alTimerTaskWidget.close()
self.__alTimerTaskWidget.deleteLater()
if self.__alConfigWidget:
self.__alConfigWidget.close()
# the config widget is already deleted in the 'self.onConfigWidgetClosed'
super().closeEvent(event)
@@ -193,21 +205,68 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
self
):
self.__timer = QTimer()
self.__timer.timeout.connect(self.pollMsgQueue)
self.__timer.start(100)
self.__msg_queue_timer = QTimer()
self.__msg_queue_timer.timeout.connect(self.pollMsgQueue)
self.__msg_queue_timer.start(100)
def startTimerTaskPolling(
self
):
self.__timer_task_timer = QTimer()
self.__timer_task_timer.timeout.connect(self.pollTimerTaskQueue)
self.__timer_task_timer.start(500)
def pollTimerTaskQueue(
self
):
if self.__is_running_timer_task:
return
try:
while not self.__is_running_timer_task:
timer_task = self.__timer_task_queue.get_nowait()
self.timerTaskIsRunning.emit(timer_task)
self.__timer_task_timer.stop()
self.__is_running_timer_task = True
self.setControlButtons(True, True, False)
if not timer_task["silent"]:
self.TrayIcon.showMessage(
"定时任务 - AutoLibrary",
f"\n已开始执行定时任务: \n{timer_task['name']}",
QSystemTrayIcon.MessageIcon.Information,
1000
)
self.showNormal()
self.__current_timer_task_thread = TimerTaskWorker(
timer_task,
self.__input_queue,
self.__output_queue,
self.__config_paths
)
self.__current_timer_task_thread.finishedSignal_TimerWorker.connect(self.onTimerTaskFinished)
self.__current_timer_task_thread.start()
except queue.Empty:
self.__is_running_timer_task = False
pass
def setControlButtons(
self,
config_button_enabled: bool,
start_button_enabled: bool,
stop_button_enabled: bool
stop_button_enabled: bool,
start_button_enabled: bool
):
self.ConfigButton.setEnabled(config_button_enabled)
self.StartButton.setEnabled(start_button_enabled)
self.StopButton.setEnabled(stop_button_enabled)
# if the enable is None, then keep the original state
if config_button_enabled is not None:
self.ConfigButton.setEnabled(config_button_enabled)
if stop_button_enabled is not None:
self.StopButton.setEnabled(stop_button_enabled)
if start_button_enabled is not None:
self.StartButton.setEnabled(start_button_enabled)
@Slot()
def showMsg(
@@ -215,7 +274,7 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
msg: str
):
self.appendToTextEdit(f"[{self.__class_name:<12}] >>> : {msg}")
self.__output_queue.put(f"[{self.__class_name:<15}] >>> : {msg}")
@Slot()
def showTrace(
@@ -224,7 +283,7 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
):
timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
self.appendToTextEdit(f"{timestamp}-[{self.__class_name:<12}] : {msg}")
self.__output_queue.put(f"{timestamp}-[{self.__class_name:<15}] : {msg}")
@Slot()
def pollMsgQueue(
@@ -238,6 +297,13 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
except queue.Empty:
pass
@Slot()
def onTimerTaskWidgetClosed(
self
):
self.TimerTaskWidgetButton.setEnabled(True)
@Slot(dict)
def onConfigWidgetClosed(
self,
@@ -248,11 +314,56 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
self.__alConfigWidget.configWidgetCloseSingal.disconnect(self.onConfigWidgetClosed)
self.__alConfigWidget.deleteLater()
self.__alConfigWidget = None
self.ConfigButton.setEnabled(True)
self.StartButton.setEnabled(True)
self.StopButton.setEnabled(False)
self.setControlButtons(True, None, None)
self.__config_paths = config_paths
@Slot(dict)
def onTimerTaskIsReady(
self,
timer_task: dict
):
self.__timer_task_queue.put(timer_task)
@Slot(dict)
def onTimerTaskFinished(
self,
is_error: bool,
timer_task: dict
):
self.__current_timer_task_thread.wait(1000)
self.__current_timer_task_thread.finishedSignal_TimerWorker.disconnect(self.onTimerTaskFinished)
self.__current_timer_task_thread.deleteLater()
self.__current_timer_task_thread = None
self.setControlButtons(None, False, True)
self.__is_running_timer_task = False
self.__timer_task_timer.start(500)
timer_task["executed"] = True
self.TrayIcon.showMessage(
"定时任务 - AutoLibrary",
f"\n定时任务 '{timer_task['name']}' 执行{'失败' if is_error else '完成'}",
QSystemTrayIcon.MessageIcon.Information,
1000
)
self.showTrace(
f"定时任务 {timer_task['name']} 执行{'失败' if is_error else '完成'}, uuid: {timer_task['task_uuid']}"
)
if not is_error:
self.timerTaskIsExecuted.emit(timer_task)
else:
self.timerTaskIsError.emit(timer_task)
@Slot()
def onTimerTaskWidgetButtonClicked(
self
):
self.__alTimerTaskWidget.show()
self.__alTimerTaskWidget.raise_()
self.__alTimerTaskWidget.activateWindow()
self.TimerTaskWidgetButton.setEnabled(False)
@Slot()
def onConfigButtonClicked(
self
@@ -264,8 +375,6 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
self.__config_paths
)
self.__alConfigWidget.configWidgetCloseSingal.connect(self.onConfigWidgetClosed)
self.__alConfigWidget.setWindowFlags(Qt.Window)
self.__alConfigWidget.setWindowModality(Qt.ApplicationModal)
self.__alConfigWidget.show()
self.__alConfigWidget.raise_()
self.__alConfigWidget.activateWindow()
@@ -276,16 +385,15 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
self
):
self.setControlButtons(False, False, True)
self.setControlButtons(None, True, False)
if self.__auto_lib_thread is None:
self.__auto_lib_thread = AutoLibWorker(
self.__input_queue,
self.__output_queue,
self.__config_paths,
self.__config_paths
)
self.__auto_lib_thread.finishedSignal.connect(self.onStopButtonClicked)
self.__auto_lib_thread.showMsgSignal.connect(self.showMsg)
self.__auto_lib_thread.showTraceSignal.connect(self.showTrace)
self.__auto_lib_thread.finishedWithErrorSignal.connect(self.onStopButtonClicked)
self.__auto_lib_thread.start()
@Slot()
@@ -295,15 +403,13 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
if self.__auto_lib_thread:
self.showTrace("正在停止操作......")
self.__auto_lib_thread.stop()
self.__auto_lib_thread.wait()
self.__auto_lib_thread.wait(2000)
self.showTrace("操作已停止")
self.__auto_lib_thread.showMsgSignal.disconnect(self.showMsg)
self.__auto_lib_thread.showTraceSignal.disconnect(self.showTrace)
self.__auto_lib_thread.finishedSignal.disconnect(self.onStopButtonClicked)
self.__auto_lib_thread.finishedWithErrorSignal.disconnect(self.onStopButtonClicked)
self.__auto_lib_thread.deleteLater()
self.__auto_lib_thread = None
self.setControlButtons(True, True, False)
self.setControlButtons(None, False, True)
@Slot()
def onSendButtonClicked(
@@ -314,4 +420,5 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
if not msg:
return
self.showMsg(msg)
self.__input_queue.put(msg) # put message to input queue
self.MessageEdit.clear()
+48 -2
View File
@@ -50,11 +50,33 @@
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="QPushButton" name="TimerTaskWidgetButton">
<property name="minimumSize">
<size>
<width>25</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>25</width>
<height>25</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset theme="document-open-recent"/>
</property>
</widget>
</item>
<item>
<widget class="QFrame" name="ControlSpaceFrame">
<property name="minimumSize">
<size>
<width>1280</width>
<width>1000</width>
<height>0</height>
</size>
</property>
@@ -237,6 +259,9 @@ font: 700 9pt &quot;Microsoft YaHei UI&quot;;</string>
<property name="text">
<string>发送</string>
</property>
<property name="icon">
<iconset theme="document-send"/>
</property>
</widget>
</item>
</layout>
@@ -245,7 +270,7 @@ font: 700 9pt &quot;Microsoft YaHei UI&quot;;</string>
</widget>
<widget class="QMenuBar" name="MenuBar">
<property name="enabled">
<bool>false</bool>
<bool>true</bool>
</property>
<property name="geometry">
<rect>
@@ -258,12 +283,33 @@ font: 700 9pt &quot;Microsoft YaHei UI&quot;;</string>
<property name="nativeMenuBar">
<bool>true</bool>
</property>
<widget class="QMenu" name="HelpMenu">
<property name="mouseTracking">
<bool>true</bool>
</property>
<property name="title">
<string>帮助</string>
</property>
<addaction name="ManualAction"/>
<addaction name="AboutAction"/>
</widget>
<addaction name="HelpMenu"/>
</widget>
<widget class="QStatusBar" name="StatusBar">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
<action name="ManualAction">
<property name="text">
<string>在线手册</string>
</property>
</action>
<action name="AboutAction">
<property name="text">
<string>关于</string>
</property>
</action>
</widget>
<resources/>
<connections/>
+170
View File
@@ -0,0 +1,170 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2025 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details.
"""
import os
import time
import queue
from PySide6.QtCore import (
Slot, Signal, QThread
)
from base.MsgBase import MsgBase
from operators.AutoLib import AutoLib
from utils.ConfigReader import ConfigReader
class AutoLibWorker(QThread, MsgBase):
finishedSignal = Signal()
finishedWithErrorSignal = Signal()
def __init__(
self,
input_queue: queue.Queue,
output_queue: queue.Queue,
config_paths: dict
):
super().__init__(input_queue = input_queue, output_queue = output_queue)
self.__config_paths = config_paths
def checkTimeAvailable(
self,
) -> bool:
current_time = time.strftime("%H:%M", time.localtime())
if current_time >= "23:30" or current_time <= "07:30":
self._showTrace(
"当前时间不在图书馆开放时间内, 请在 07:30 - 23:30 之间尝试"
)
return False
return True
def checkConfigPaths(
self,
) -> bool:
if not all(
os.path.exists(path) for path in self.__config_paths.values()
):
self._showTrace("配置文件路径不存在, 请检查配置文件路径是否正确")
return False
return True
def loadConfigs(
self
) -> bool:
self._showTrace(
f"正在加载配置文件, 运行配置文件路径: {self.__config_paths["run"]}"
)
self.__run_config = ConfigReader(
self.__config_paths["run"]
).getConfigs()
self._showTrace(
f"正在加载配置文件, 用户配置文件路径: {self.__config_paths["user"]}"
)
self.__user_config = ConfigReader(
self.__config_paths["user"]
).getConfigs()
if self.__run_config is None or self.__user_config is None:
self._showTrace("配置文件加载失败, 请检查配置文件是否正确")
self._showTrace("配置文件加载失败, 请检查配置文件是否正确")
return False
if not self.__user_config.get("groups"):
self._showTrace(
"用户配置文件中无有效任务组, 请检查用户配置文件是否正确"
)
return False
return True
def run(
self
):
auto_lib = None
self._showTrace("AutoLibrary 开始运行")
if not self.checkTimeAvailable()\
or not self.checkConfigPaths():
# time or config existence check failed, skip and finish
pass
else:
try:
if not self.loadConfigs():
raise Exception("配置文件加载失败")
auto_lib = AutoLib(
self._input_queue,
self._output_queue,
self.__run_config
)
groups = self.__user_config.get("groups")
for group in groups:
if not group["enabled"]:
self._showTrace(f"任务组 {group["name"]} 已跳过")
continue
self._showTrace(f"正在运行任务组 {group["name"]}")
auto_lib.run(
{ "users": group.get("users", []) }
)
except Exception as e:
self._showTrace(f"AutoLibrary 运行时发生异常 : {e}")
self.finishedWithErrorSignal.emit()
return
if auto_lib:
auto_lib.close()
self._showTrace("AutoLibrary 运行结束")
self.finishedSignal.emit()
class TimerTaskWorker(AutoLibWorker):
finishedSignal_TimerWorker = Signal(bool, dict)
def __init__(
self,
timer_task: dict,
input_queue: queue.Queue,
output_queue: queue.Queue,
config_paths: dict
):
super().__init__(input_queue, output_queue, config_paths)
self.__timer_task = timer_task
self.finishedSignal.connect(self.onTimerTaskIsFinished)
self.finishedWithErrorSignal.connect(self.onTimerTaskIsError)
def run(
self
):
self._showTrace(f"定时任务 {self.__timer_task['name']} 开始运行")
super().run()
@Slot(dict)
def onTimerTaskIsError(
self
):
self._showTrace(f"定时任务 {self.__timer_task['name']} 运行时发生异常")
self.finishedSignal_TimerWorker.emit(True, self.__timer_task)
@Slot(dict)
def onTimerTaskIsFinished(
self
):
self._showTrace(f"定时任务 {self.__timer_task['name']} 运行结束")
self.finishedSignal_TimerWorker.emit(False, self.__timer_task)
@@ -15,7 +15,7 @@ from PySide6.QtWidgets import (
)
class SeatFrame(QFrame):
class ALSeatFrame(QFrame):
clicked = Signal(str)
@@ -28,9 +28,9 @@ class SeatFrame(QFrame):
super().__init__(parent)
self.__seat_number = seat_number
self.__is_selected = False
self.setUpUi()
self.setupUi()
def setUpUi(
def setupUi(
self
):
@@ -18,10 +18,10 @@ from PySide6.QtWidgets import (
from PySide6.QtGui import (
QPainter, QWheelEvent, QCloseEvent
)
from gui.SeatFrame import SeatFrame
from gui.ALSeatFrame import ALSeatFrame
class SeatMapWidget(QWidget):
class ALSeatMapWidget(QWidget):
seatMapWidgetClosed = Signal(list)
@@ -40,7 +40,7 @@ class SeatMapWidget(QWidget):
self.__seats_data = seats_data
self.__selected_seats = []
self.__seat_frames = {}
self.setUpUi()
self.setupUi()
self.connectSignals()
@staticmethod
@@ -55,7 +55,7 @@ class SeatMapWidget(QWidget):
return seat_number.zfill(3)
def setUpUi(
def setupUi(
self
):
@@ -115,6 +115,30 @@ class SeatMapWidget(QWidget):
self.CancelButton.clicked.connect(self.onCancelButtonClicked)
def showEvent(
self,
event
):
result = super().showEvent(event)
screen_rect = self.screen().geometry()
target_pos = self.parent().geometry().center()
target_pos.setX(target_pos.x() - self.width()//2)
target_pos.setY(target_pos.y() - self.height()//2)
if target_pos.x() < 0:
target_pos.setX(0)
if target_pos.x() + self.width() > screen_rect.width():
target_pos.setX(screen_rect.width() - self.width())
if target_pos.y() < 0:
target_pos.setY(0)
if target_pos.y() + self.height() > screen_rect.height():
target_pos.setY(screen_rect.height() - self.height())
self.move(target_pos)
return result
def closeEvent(
self,
event: QCloseEvent
@@ -160,7 +184,7 @@ class SeatMapWidget(QWidget):
seats_number = [seat.strip() for seat in row.split(",")]
for seat_number in seats_number:
if seat_number:
seat_widget = SeatFrame(seat_number)
seat_widget = ALSeatFrame(seat_number)
seat_widget.clicked.connect(self.onSeatClicked)
self.SeatsContainerLayout.addWidget(seat_widget, row_idx, col_idx)
self.__seat_frames[seat_number] = seat_widget
+509
View File
@@ -0,0 +1,509 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2025 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details.
"""
import os
import sys
import time
import copy
import queue
from enum import Enum
from datetime import datetime, timedelta
from PySide6.QtCore import (
Qt, Signal, Slot, QTimer
)
from PySide6.QtWidgets import (
QDialog, QWidget, QListWidgetItem, QMessageBox,
QHBoxLayout, QVBoxLayout, QLabel, QPushButton
)
from PySide6.QtGui import (
QCloseEvent, QScreen
)
from gui.Ui_ALTimerTaskWidget import Ui_ALTimerTaskWidget
from gui.ALAddTimerTaskDialog import ALAddTimerTaskWidget, TimerTaskStatus
from utils.ConfigReader import ConfigReader
from utils.ConfigWriter import ConfigWriter
class SortPolicy(Enum):
BY_NAME = "按名称"
BY_ADD_TIME = "按添加时间"
BY_EXECUTE_TIME = "按执行时间"
class TimerTaskItemWidget(QWidget):
def __init__(
self,
parent = None,
timer_task: dict = None
):
super().__init__(parent)
self.__timer_task = timer_task
self.modifyUi()
def modifyUi(
self
):
self.ItemWidgetLayout = QHBoxLayout(self)
self.ItemWidgetLayout.setSpacing(10)
self.ItemWidgetLayout.setContentsMargins(10, 5, 10, 5)
self.TaskInfoLayout = QVBoxLayout()
self.TaskInfoLayout.setSpacing(5)
TaskNameLabel = QLabel(self.__timer_task["name"])
TaskNameLabelFont = TaskNameLabel.font()
TaskNameLabelFont.setBold(True)
TaskNameLabel.setFont(TaskNameLabelFont)
TaskNameLabel.setFixedHeight(25)
self.TaskInfoLayout.addWidget(TaskNameLabel)
ExecuteTimeStr = self.__timer_task["execute_time"].strftime("%Y-%m-%d %H:%M:%S")
ExecuteTimeLabel = QLabel(f"执行时间: {ExecuteTimeStr}")
ExecuteTimeLabel.setStyleSheet("color: gray;")
ExecuteTimeLabel.setFixedHeight(20)
self.TaskInfoLayout.addWidget(ExecuteTimeLabel)
self.ItemWidgetLayout.addLayout(self.TaskInfoLayout)
self.ItemWidgetLayout.addStretch()
match self.__timer_task["status"]:
case TimerTaskStatus.PENDING:
TaskStatusText = "等待中"
TaskStatusColor = "#FF9800"
case TimerTaskStatus.READY:
TaskStatusText = "已就绪"
TaskStatusColor = "#316BFF"
case TimerTaskStatus.RUNNING:
TaskStatusText = "执行中"
TaskStatusColor = "#2294FF"
case TimerTaskStatus.EXECUTED:
TaskStatusText = "已执行"
TaskStatusColor = "#4CAF50"
case TimerTaskStatus.ERROR:
TaskStatusText = "执行失败"
TaskStatusColor = "#FF5722"
case TimerTaskStatus.OUTDATED:
TaskStatusText = "已过期"
TaskStatusColor = "#FF5722"
TaskStatusLabel = QLabel(TaskStatusText)
TaskStatusLabel.setStyleSheet(f"""
QLabel {{
background-color: {TaskStatusColor};
color: white;
border-radius: 5px;
font-weight: bold;
}}
""")
TaskStatusLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
TaskStatusLabel.setFixedSize(80, 25)
self.ItemWidgetLayout.addWidget(TaskStatusLabel)
TaskModeText = "静默" if self.__timer_task["silent"] else "显示"
TaskModeColor = "#6325FF" if self.__timer_task["silent"] else "#2294FF"
TaskModeLabel = QLabel(TaskModeText)
TaskModeLabel.setStyleSheet(f"""
QLabel {{
background-color: {TaskModeColor};
color: white;
border-radius: 5px;
font-weight: bold;
}}
""")
TaskModeLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
TaskModeLabel.setFixedSize(60, 25)
self.ItemWidgetLayout.addWidget(TaskModeLabel)
self.DeleteButton = QPushButton("删除")
self.DeleteButton.setFixedSize(80, 25)
self.ItemWidgetLayout.addWidget(self.DeleteButton)
if self.__timer_task["status"] == TimerTaskStatus.READY\
or self.__timer_task["status"] == TimerTaskStatus.RUNNING:
self.DeleteButton.setEnabled(False)
self.setFixedHeight(55)
class ALTimerTaskWidget(QWidget, Ui_ALTimerTaskWidget):
timerTasksChanged = Signal()
timerTaskIsReady = Signal(dict)
timerTaskWidgetClosed = Signal()
def __init__(
self,
parent = None,
timer_tasks_config_path: str = ""
):
super().__init__(parent)
self.__timer_tasks = []
self.__check_timer = None
self.__sort_policy = SortPolicy.BY_EXECUTE_TIME
self.__sort_order = Qt.SortOrder.AscendingOrder
self.__timer_tasks_config_path = timer_tasks_config_path
self.setupUi(self)
self.connectSignals()
self.setupTimer()
if not self.initializeTimerTasks():
return
def connectSignals(
self
):
self.AddTimerTaskButton.clicked.connect(self.addTask)
self.ClearAllTimerTasksButton.clicked.connect(self.clearAllTasks)
self.TimerTaskSortTypeComboBox.currentIndexChanged.connect(self.onSortPolicyComboBoxChanged)
self.TimerTaskSortOrderToggleButton.clicked.connect(self.onSortOrderToggleButtonClicked)
self.timerTasksChanged.connect(self.onTimerTasksChanged)
def setupTimer(
self
):
self.__check_timer = QTimer(self)
self.__check_timer.timeout.connect(self.checkTasks)
self.__check_timer.start(500)
def initializeTimerTasks(
self
) -> bool:
timer_tasks = self.loadTimerTasks(self.__timer_tasks_config_path)
if timer_tasks is not None:
self.__timer_tasks = timer_tasks
self.timerTasksChanged.emit()
return True
timer_tasks = []
if self.saveTimerTasks(self.__timer_tasks_config_path, copy.deepcopy(timer_tasks)):
QMessageBox.information(
self,
"信息 - AutoLibrary",
f"定时任务配置文件初始化完成: \n{self.__timer_tasks_config_path}"
)
self.__timer_tasks = timer_tasks
self.updateTimerTaskList()
return True
return False
def loadTimerTasks(
self,
timer_tasks_config_path: str
) -> list:
try:
if not timer_tasks_config_path or not os.path.exists(timer_tasks_config_path):
raise Exception("定时任务配置文件不存在")
timer_tasks = ConfigReader(timer_tasks_config_path).getConfigs()
if timer_tasks and "timer_tasks" in timer_tasks:
for task in timer_tasks["timer_tasks"]:
task["add_time"] = datetime.strptime(task["add_time"], "%Y-%m-%d %H:%M:%S")
task["execute_time"] = datetime.strptime(task["execute_time"], "%Y-%m-%d %H:%M:%S")
task["status"] = TimerTaskStatus(task["status"])
return timer_tasks["timer_tasks"]
raise Exception("定时任务配置文件格式错误")
except Exception as e:
QMessageBox.warning(
self,
"警告 - AutoLibrary",
f"加载定时任务配置发生错误 ! : {e}\n"\
f"文件路径: {timer_tasks_config_path}"
)
return None
def saveTimerTasks(
self,
timer_tasks_config_path: str,
timer_tasks: list
) -> bool:
try:
if not timer_tasks_config_path:
raise Exception("配置文件路径为空")
for task in timer_tasks:
task["add_time"] = task["add_time"].strftime("%Y-%m-%d %H:%M:%S")
task["execute_time"] = task["execute_time"].strftime("%Y-%m-%d %H:%M:%S")
task["status"] = task["status"].value
ConfigWriter(
timer_tasks_config_path,
{ "timer_tasks": timer_tasks }
)
return True
except Exception as e:
QMessageBox.warning(
self,
"警告 - AutoLibrary",
f"保存定时任务配置发生错误 ! : {e}\n"\
f"文件路径: {timer_tasks_config_path}"
)
return False
def showEvent(
self,
event
):
result = super().showEvent(event)
screen_rect = self.screen().geometry()
target_pos = self.parent().geometry().center()
target_pos.setX(target_pos.x() - self.width()//2)
target_pos.setY(target_pos.y() - self.height()//2)
if target_pos.x() < 0:
target_pos.setX(0)
if target_pos.x() + self.width() > screen_rect.width():
target_pos.setX(screen_rect.width() - self.width())
if target_pos.y() < 0:
target_pos.setY(0)
if target_pos.y() + self.height() > screen_rect.height():
target_pos.setY(screen_rect.height() - self.height())
self.move(target_pos)
return result
def closeEvent(
self,
event: QCloseEvent
):
self.hide()
self.timerTaskWidgetClosed.emit()
event.ignore()
def sortTimerTasks(
self,
policy: SortPolicy = SortPolicy.BY_EXECUTE_TIME,
order: Qt.SortOrder = Qt.SortOrder.AscendingOrder
):
if policy == SortPolicy.BY_NAME:
self.__timer_tasks.sort(
key = lambda x: x["name"],
reverse = order is Qt.SortOrder.DescendingOrder
)
elif policy == SortPolicy.BY_ADD_TIME:
self.__timer_tasks.sort(
key = lambda x: x["add_time"],
reverse = order is Qt.SortOrder.DescendingOrder
)
elif policy == SortPolicy.BY_EXECUTE_TIME:
self.__timer_tasks.sort(
key = lambda x: x["execute_time"],
reverse = order is Qt.SortOrder.DescendingOrder
)
def updateStat(
self
):
pending = 0
in_queue = 0
executed = 0
invalid = 0
total = len(self.__timer_tasks)
for timer_task in self.__timer_tasks:
if timer_task["status"] == TimerTaskStatus.PENDING:
pending += 1
elif timer_task["status"] == TimerTaskStatus.READY\
or timer_task["status"] == TimerTaskStatus.RUNNING:
in_queue += 1
elif timer_task["status"] == TimerTaskStatus.EXECUTED:
executed += 1
elif timer_task["status"] == TimerTaskStatus.ERROR\
or timer_task["status"] == TimerTaskStatus.OUTDATED:
invalid += 1
self.TotalTaskLabel.setText(f"总任务:{total}")
self.PendingTaskLabel.setText(f"待执行:{pending}")
self.InQueueTaskLabel.setText(f"队列中:{in_queue}")
self.ExecutedTaskLabel.setText(f"已执行:{executed}")
self.InvalidTaskLabel.setText(f"无效的:{invalid}")
def updateTimerTaskList(
self
):
self.TimerTasksListWidget.clear()
self.sortTimerTasks(self.__sort_policy, self.__sort_order)
for timer_task in self.__timer_tasks:
item = QListWidgetItem()
item.setData(Qt.UserRole, timer_task)
widget = TimerTaskItemWidget(self, timer_task)
widget.DeleteButton.clicked.connect(
lambda _, uuid = timer_task["task_uuid"]: self.deleteTask(uuid)
)
item.setSizeHint(widget.size())
self.TimerTasksListWidget.addItem(item)
self.TimerTasksListWidget.setItemWidget(item, widget)
def addTask(
self
):
dialog = ALAddTimerTaskWidget(self)
if dialog.exec() == QDialog.DialogCode.Accepted:
timer_task = dialog.getTimerTask()
self.__timer_tasks.append(timer_task)
self.timerTasksChanged.emit()
def deleteTask(
self,
task_uuid: str
):
self.__timer_tasks = [
x for x in self.__timer_tasks
if x["task_uuid"] != task_uuid
]
self.timerTasksChanged.emit()
def clearAllTasks(
self
):
if not self.__timer_tasks:
return
result = QMessageBox.question(
self,
"确认 - AutoLibrary",
"是否要清除所有定时任务 ?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if result is QMessageBox.StandardButton.No:
return
in_queue_tasks = [
x for x in self.__timer_tasks
if x["status"] == TimerTaskStatus.READY
or x["status"] == TimerTaskStatus.RUNNING
]
in_queue_count = len(in_queue_tasks)
if in_queue_count > 0:
QMessageBox.warning(
self,
"警告 - AutoLibrary",
"存在正在执行或已就绪的队列任务,无法清除所有定时任务 !"
)
self.__timer_tasks = in_queue_tasks
self.timerTasksChanged.emit()
def checkTasks(
self
):
need_update = False
now = datetime.now()
for timer_task in self.__timer_tasks:
if timer_task["execute_time"] > now:
continue
if timer_task["status"] is not TimerTaskStatus.PENDING:
continue
if timer_task["execute_time"] <= now + timedelta(seconds = -5):
timer_task["status"] = TimerTaskStatus.OUTDATED
need_update = True
else:
timer_task["status"] = TimerTaskStatus.READY
self.timerTaskIsReady.emit(timer_task)
need_update = True
if need_update:
self.timerTasksChanged.emit()
@Slot(int)
def onSortPolicyComboBoxChanged(
self,
policy: int
):
mapping = {
0: SortPolicy.BY_NAME,
1: SortPolicy.BY_ADD_TIME,
2: SortPolicy.BY_EXECUTE_TIME
}
self.__sort_policy = mapping[policy]
self.updateTimerTaskList()
@Slot()
def onSortOrderToggleButtonClicked(
self
):
self.__sort_order = Qt.SortOrder.AscendingOrder\
if self.__sort_order is Qt.SortOrder.DescendingOrder\
else Qt.SortOrder.DescendingOrder
self.TimerTaskSortOrderToggleButton.setText(
"" if self.__sort_order is Qt.SortOrder.AscendingOrder else ""
)
self.updateTimerTaskList()
@Slot()
def onTimerTasksChanged(
self
):
self.saveTimerTasks(self.__timer_tasks_config_path, copy.deepcopy(self.__timer_tasks))
self.updateTimerTaskList()
self.updateStat()
@Slot(dict)
def onTimerTaskIsRunning(
self,
timer_task: dict
):
for task in self.__timer_tasks:
if task["task_uuid"] == timer_task["task_uuid"]:
task["status"] = TimerTaskStatus.RUNNING
self.timerTasksChanged.emit()
@Slot(dict)
def onTimerTaskIsExecuted(
self,
timer_task: dict
):
for task in self.__timer_tasks:
if task["task_uuid"] == timer_task["task_uuid"]:
task["status"] = TimerTaskStatus.EXECUTED
self.timerTasksChanged.emit()
@Slot(dict)
def onTimerTaskIsError(
self,
timer_task: dict
):
for task in self.__timer_tasks:
if task["task_uuid"] == timer_task["task_uuid"]:
task["status"] = TimerTaskStatus.ERROR
self.timerTasksChanged.emit()
+358
View File
@@ -0,0 +1,358 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ALTimerTaskWidget</class>
<widget class="QWidget" name="ALTimerTaskWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>400</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>400</width>
<height>400</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>600</width>
<height>400</height>
</size>
</property>
<property name="windowTitle">
<string>定时任务 - AutoLibrary</string>
</property>
<layout class="QVBoxLayout" name="ALTimerTaskWidgetLayout">
<property name="spacing">
<number>5</number>
</property>
<property name="leftMargin">
<number>5</number>
</property>
<property name="topMargin">
<number>5</number>
</property>
<property name="rightMargin">
<number>5</number>
</property>
<property name="bottomMargin">
<number>5</number>
</property>
<item>
<layout class="QHBoxLayout" name="TimerTaskStatusLayout">
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="QLabel" name="TotalTaskLabel">
<property name="minimumSize">
<size>
<width>70</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>70</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>总任务:0</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="PendingTaskLabel">
<property name="minimumSize">
<size>
<width>70</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>70</width>
<height>25</height>
</size>
</property>
<property name="styleSheet">
<string notr="true">QLabel {
color: #FF9800
}</string>
</property>
<property name="text">
<string>待执行:0</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="InQueueTaskLabel">
<property name="minimumSize">
<size>
<width>70</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>70</width>
<height>25</height>
</size>
</property>
<property name="styleSheet">
<string notr="true">QLabel {
color: #2294FF
}</string>
</property>
<property name="text">
<string>队列中:0</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="ExecutedTaskLabel">
<property name="minimumSize">
<size>
<width>70</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>70</width>
<height>25</height>
</size>
</property>
<property name="styleSheet">
<string notr="true">QLabel {
color: #4CAF50
}</string>
</property>
<property name="text">
<string>已执行:0</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="InvalidTaskLabel">
<property name="minimumSize">
<size>
<width>70</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>25</width>
<height>70</height>
</size>
</property>
<property name="styleSheet">
<string notr="true">QLabel {
color: #FF5722
}</string>
</property>
<property name="text">
<string>无效的:0</string>
</property>
</widget>
</item>
<item>
<widget class="QFrame" name="TimerTaskSpaceFrame">
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>600</width>
<height>16777215</height>
</size>
</property>
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Plain</enum>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="TimerTaskSortLayout">
<property name="spacing">
<number>0</number>
</property>
<item>
<widget class="QFrame" name="TimerTaskSortSpaceFrame">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>16777215</height>
</size>
</property>
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Plain</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="TimerTaskSortLabel">
<property name="minimumSize">
<size>
<width>40</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>40</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>排序:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="TimerTaskSortTypeComboBox">
<property name="minimumSize">
<size>
<width>90</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>90</width>
<height>25</height>
</size>
</property>
<item>
<property name="text">
<string>按名称</string>
</property>
</item>
<item>
<property name="text">
<string>按添加时间</string>
</property>
</item>
<item>
<property name="text">
<string>按执行时间</string>
</property>
</item>
</widget>
</item>
<item>
<widget class="QPushButton" name="TimerTaskSortOrderToggleButton">
<property name="minimumSize">
<size>
<width>25</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>25</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>↑</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QListWidget" name="TimerTasksListWidget">
<property name="enabled">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="TimerTaskEditLayout">
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="QPushButton" name="ClearAllTimerTasksButton">
<property name="minimumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>清除全部</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="AddTimerTaskButton">
<property name="minimumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>添加任务</string>
</property>
</widget>
</item>
<item>
<widget class="QFrame" name="TimerTaskEditSpaceFrame">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>16777215</height>
</size>
</property>
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Plain</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
+150
View File
@@ -0,0 +1,150 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2025 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details.
"""
from enum import Enum
from PySide6.QtCore import (
Qt, QSize, QCoreApplication, QRect, QPoint
)
from PySide6.QtWidgets import (
QAbstractScrollArea, QAbstractItemView,
QTreeWidget, QTreeWidgetItem
)
from PySide6.QtGui import (
QDragEnterEvent, QDragMoveEvent, QDropEvent
)
class TreeItemType(Enum):
GROUP = 0
USER = 1
class ALUserTreeWidget(QTreeWidget):
def __init__(
self,
parent = None
):
super().__init__(parent)
self.setupUi()
self.translateUi()
def setupUi(
self
):
__qtreewidgetitem = QTreeWidgetItem()
__qtreewidgetitem.setText(0, u"\u5206\u7ec4/\u7528\u6237");
self.setHeaderItem(__qtreewidgetitem)
self.setObjectName(u"UserTreeWidget")
self.setMinimumSize(QSize(230, 0))
self.setMaximumSize(QSize(250, 16777215))
self.setSizeAdjustPolicy(QAbstractScrollArea.SizeAdjustPolicy.AdjustIgnored)
self.setTabKeyNavigation(True)
self.setDragEnabled(True)
self.setAcceptDrops(True)
self.setDropIndicatorShown(True)
self.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove)
self.setDefaultDropAction(Qt.DropAction.IgnoreAction)
self.setAlternatingRowColors(True)
self.setSortingEnabled(True)
self.sortByColumn(0, Qt.SortOrder.AscendingOrder)
self.setAnimated(True)
self.setAllColumnsShowFocus(False)
self.setHeaderHidden(False)
self.setColumnCount(2)
self.setColumnWidth(0, 150)
self.setColumnWidth(1, 20)
self.header().setCascadingSectionResizes(False)
self.header().setHighlightSections(False)
self.header().setProperty(u"showSortIndicator", True)
def translateUi(
self
):
___qtreewidgetitem = self.headerItem()
___qtreewidgetitem.setText(1, QCoreApplication.translate("ALConfigWidget", u"\u72b6\u6001", None));
@staticmethod
def isDragPositionValid(
target_rect: QRect,
drag_pos: QPoint,
) -> bool:
y_offset = drag_pos.y() - target_rect.top()
valid = (y_offset > target_rect.height()*0.2 and
y_offset < target_rect.height()*0.8)
return valid
def dragEnterEvent(
self,
event: QDragEnterEvent
):
super().dragEnterEvent(event)
def dragMoveEvent(
self,
event: QDragMoveEvent
):
super().dragMoveEvent(event)
source_item = self.currentItem()
target_item = self.itemAt(event.position().toPoint())
if source_item is None:
event.ignore()
return
if source_item.type() == TreeItemType.GROUP.value:
if target_item is not None:
event.ignore()
return
elif source_item.type() == TreeItemType.USER.value:
if target_item is None:
event.ignore()
return
if target_item.type() != TreeItemType.GROUP.value:
event.ignore()
return
if target_item.checkState(1) == Qt.CheckState.Unchecked:
event.ignore()
return
if not self.isDragPositionValid(
self.visualItemRect(target_item),
event.position().toPoint()
):
event.ignore()
return
else:
event.ignore()
return
event.acceptProposedAction()
def dropEvent(
self,
event: QDropEvent
):
super().dropEvent(event)
for item_index in range(self.topLevelItemCount()):
self.topLevelItem(item_index).setExpanded(True)
self.setCurrentItem(None)
+16
View File
@@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
"""
The contents of this file will automatically be updated by the
workflow process. Do not edit manually.
This file is auto-generated during the workflow process.
Last updated: 2026-01-05 04:04:54 UTC
"""
AL_VERSION = "1.0.2"
AL_TAG = "v1.0.2"
AL_COMMIT_SHA = "local"
AL_COMMIT_DATE = "null" # time zone : UTC
AL_BUILD_DATE = "null" # time zone : UTC
AL_VERSION_FULL = f"{AL_VERSION} ({AL_COMMIT_SHA})"
+1 -1
View File
@@ -1,6 +1,6 @@
<RCC>
<qresource prefix="/res/icon">
<file>icons/AutoLibrary.ico</file>
<file>icons/AutoLibrary_32x32.ico</file>
</qresource>
<qresource prefix="/res/trans">
<file>translators/qtbase_zh_CN.qm</file>
+62
View File
@@ -0,0 +1,62 @@
@echo off
chcp 65001 >nul
setlocal enabledelayedexpansion
cd /d "%~dp0.."
echo [AutoLibrary compile] 检查翻译文件...
if exist translators (
cd translators
set ts_count=0
for %%f in (*.ts) do set /a ts_count+=1
if !ts_count! gtr 0 (
echo [AutoLibrary compile] 找到 !ts_count! 个 .ts 文件,开始编译翻译文件...
for %%f in (*.ts) do (
set "qm_filename=%%~nf.qm"
echo [AutoLibrary compile] 正在编译翻译文件: "%%f" -> "!qm_filename!"
pyside6-lrelease "%%f"
if !errorlevel! equ 0 (
echo [AutoLibrary compile] 翻译文件 "%%f" ✓ 编译成功,输出文件: "!qm_filename!"
) else (
echo [AutoLibrary compile] 翻译文件 "%%f" ✗ 编译失败
)
)
echo.
) else (
echo [AutoLibrary compile] 未找到任何 .ts 翻译文件
)
cd ..
) else (
echo [AutoLibrary compile] 未找到 translators 目录
)
echo.
set count=0
for %%f in (*.qrc) do set /a count+=1
if %count% equ 0 (
echo [AutoLibrary compile] 错误: 未找到任何 .qrc 文件
pause
exit /b 1
)
echo [AutoLibrary compile] 找到 %count% 个 .qrc 文件,开始编译...
echo.
for %%f in (*.qrc) do (
set "filename=%%~nf"
set "output_file=!filename!.py"
echo [AutoLibrary compile] 正在编译: "%%f" -> "!output_file!"
pyside6-rcc "%%f" -o "!output_file!"
if !errorlevel! equ 0 (
echo [AutoLibrary compile] 文件 "%%f" ✓ 编译成功,输出文件: "!output_file!"
) else (
echo [AutoLibrary compile] 文件 "%%f" ✗ 编译失败
)
echo.
)
echo [AutoLibrary compile] 所有操作完成。
+64
View File
@@ -0,0 +1,64 @@
#!/bin/bash
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PARENT_DIR="$(dirname "$SCRIPT_DIR")"
cd "$PARENT_DIR"
echo "[AutoLibrary compile] 检查翻译文件..."
if [ -d "translators" ]; then
cd translators
ts_files=(*.ts)
ts_count=${#ts_files[@]}
# 如果第一个元素是"*.ts"(表示没有匹配),则数量为0
if [ "$ts_count" -eq 1 ] && [ "${ts_files[0]}" = "*.ts" ]; then
ts_count=0
fi
if [ $ts_count -gt 0 ]; then
echo "[AutoLibrary compile] 找到 $ts_count 个 .ts 文件,开始编译翻译文件..."
for file in *.ts; do
base_name=$(basename "$file" .ts)
qm_file="${base_name}.qm"
echo "[AutoLibrary compile] 正在编译翻译文件: \"$file\" -> \"$qm_file\""
if pyside6-lrelease "$file"; then
echo "[AutoLibrary compile] 翻译文件 \"$file\" ✓ 编译成功,输出文件: \"$qm_file\""
else
echo "[AutoLibrary compile] 翻译文件 \"$file\" ✗ 编译失败"
fi
done
echo
else
echo "[AutoLibrary compile] 未找到任何 .ts 翻译文件"
fi
cd ..
else
echo "[AutoLibrary compile] 未找到 translators 目录"
fi
echo
file_count=$(ls *.qrc 2>/dev/null | wc -l)
if [ $file_count -eq 0 ]; then
echo "[AutoLibrary compile] 错误: 未找到任何 .qrc 文件"
exit 1
fi
echo "[AutoLibrary compile] 找到 $file_count 个 .qrc 文件,开始编译..."
echo
for file in *.qrc; do
base_name=$(basename "$file" .qrc)
output_file="${base_name}.py"
echo "[AutoLibrary compile] 正在编译: \"$file\" -> \"$output_file\""
if pyside6-rcc "$file" -o "$output_file"; then
echo "[AutoLibrary compile] 文件 \"$file\" ✓ 编译成功,输出文件: \"$output_file\""
else
echo "[AutoLibrary compile] 文件 \"$file\" ✗ 编译失败"
fi
echo
done
echo "[AutoLibrary compile] 所有操作完成。"
+33
View File
@@ -0,0 +1,33 @@
@echo off
chcp 65001 >nul
setlocal enabledelayedexpansion
cd /d "%~dp0.."
set count=0
for %%f in (*.ui) do set /a count+=1
if %count% equ 0 (
echo [AutoLibrary compile] 错误: 未找到任何 .ui 文件
pause
exit /b 1
)
echo [AutoLibrary compile] 找到 %count% 个 .ui 文件,开始编译...
echo.
for %%f in (*.ui) do (
set "filename=%%~nf"
set "output_file=Ui_!filename!.py"
echo [AutoLibrary compile] 正在编译: "%%f" -> "!output_file!"
pyside6-uic "%%f" -o "!output_file!"
if !errorlevel! equ 0 (
echo [AutoLibrary compile] 文件 "%%f" ✓ 编译成功,输出文件: "!output_file!"
) else (
echo [AutoLibrary compile] 文件 "%%f" ✗ 编译失败
)
echo.
)
echo [AutoLibrary compile] 所有操作完成。
+30
View File
@@ -0,0 +1,30 @@
#!/bin/bash
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PARENT_DIR="$(dirname "$SCRIPT_DIR")"
cd "$PARENT_DIR"
file_count=$(ls *.ui 2>/dev/null | wc -l)
if [ $file_count -eq 0 ]; then
echo "[AutoLibrary compile] 错误: 未找到任何 .ui 文件"
exit 1
fi
echo "[AutoLibrary compile] 找到 $file_count 个 .ui 文件,开始编译..."
echo
for file in *.ui; do
base_name=$(basename "$file" .ui)
output_file="Ui_${base_name}.py"
echo "[AutoLibrary compile] 正在编译: \"$file\" -> \"$output_file\""
if pyside6-uic "$file" -o "$output_file"; then
echo "[AutoLibrary compile] 文件 \"$file\" ✓ 编译成功,输出文件: \"$output_file\""
else
echo "[AutoLibrary compile] 文件 \"$file\" ✗ 编译失败"
fi
echo
done
echo "[AutoLibrary compile] 所有操作完成。"
+1
View File
@@ -0,0 +1 @@
this folder is used to store the batch scripts.

Before

Width:  |  Height:  |  Size: 785 KiB

After

Width:  |  Height:  |  Size: 785 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

+113 -68
View File
@@ -14,7 +14,9 @@ from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.edge.service import Service
from selenium.webdriver.edge.service import Service as EdgeService
from selenium.webdriver.chrome.service import Service as ChromeService
from selenium.webdriver.firefox.service import Service as FirefoxService
from base.MsgBase import MsgBase
from operators.LibChecker import LibChecker
@@ -22,6 +24,7 @@ from operators.LibLogin import LibLogin
from operators.LibLogout import LibLogout
from operators.LibReserve import LibReserve
from operators.LibCheckin import LibCheckin
from operators.LibRenew import LibRenew
from utils.ConfigReader import ConfigReader
@@ -31,13 +34,20 @@ class AutoLib(MsgBase):
def __init__(
self,
input_queue: queue.Queue,
output_queue: queue.Queue
output_queue: queue.Queue,
run_config: dict
):
super().__init__(input_queue, output_queue)
self.__system_config_reader = None
self.__users_config_reader = None
self.__run_config = run_config
self.__user_config = None
self.__driver = None
if not self.__initBrowserDriver():
raise Exception("浏览器驱动初始化失败")
else:
if not self.__initDriverUrl():
raise Exception("浏览器驱动URL初始化失败")
self.__initLibOperators()
def __initBrowserDriver(
@@ -45,50 +55,75 @@ class AutoLib(MsgBase):
) -> bool:
self._showTrace("正在初始化浏览器驱动......")
edge_options = webdriver.EdgeOptions()
if self.__system_config_reader.get("web_driver/headless"):
edge_options.add_argument("--headless")
edge_options.add_argument("--disable-gpu")
edge_options.add_argument("--no-sandbox")
edge_options.add_argument("--disable-dev-shm-usage")
web_driver_config = self.__run_config.get("web_driver", None)
self.__driver_type = web_driver_config.get("driver_type")
match self.__driver_type.lower():
case "edge":
driver_options = webdriver.EdgeOptions()
case "chrome":
driver_options = webdriver.ChromeOptions()
case "firefox":
driver_options = webdriver.FirefoxOptions()
case _:
raise Exception(f"不支持的浏览器驱动类型: {self.__driver_type} !")
if not web_driver_config:
self._showTrace("未配置浏览器驱动参数 !")
return False
if web_driver_config.get("headless"):
driver_options.add_argument("--headless")
driver_options.add_argument("--disable-gpu")
driver_options.add_argument("--no-sandbox")
driver_options.add_argument("--disable-dev-shm-usage")
# must be 1920x1080, otherwise the page will cause some elements not accessible
edge_options.add_argument("--window-size=1920,1080")
edge_options.add_argument("--remote-allow-origins=*")
driver_options.add_argument("--window-size=1920,1080")
# omit ssl errors and verbose log level
edge_options.add_argument("--ignore-certificate-errors")
edge_options.add_argument("--ignore-ssl-errors")
edge_options.add_argument("--log-level=OFF")
edge_options.add_argument("--silent")
driver_options.add_argument("--ignore-certificate-errors")
driver_options.add_argument("--ignore-ssl-errors")
driver_options.add_argument("--log-level=OFF")
driver_options.add_argument("--silent")
edge_options.add_experimental_option("excludeSwitches", ["enable-automation"])
edge_options.add_experimental_option("useAutomationExtension", False)
edge_options.add_argument("--disable-blink-features=AutomationControlled")
edge_options.add_argument(
"--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) "\
"AppleWebKit/537.36 (KHTML, like Gecko) "\
"Chrome/120.0.0.0 "\
"Safari/537.36 "\
"Edg/120.0.0.0"
)
# set options for chrome and edge
if self.__driver_type.lower() in ["edge", "chrome"]:
driver_options.add_argument("--remote-allow-origins=*")
driver_options.add_experimental_option("excludeSwitches", ["enable-automation"])
driver_options.add_experimental_option("useAutomationExtension", False)
driver_options.add_argument("--disable-blink-features=AutomationControlled")
user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "\
"AppleWebKit/537.36 (KHTML, like Gecko) "\
"Chrome/120.0.0.0 "\
"Safari/537.36"
if self.__driver_type.lower() == "edge":
user_agent += " Edg/120.0.0.0"
# set options for firefox
elif self.__driver_type.lower() == "firefox":
driver_options.set_preference("dom.webdriver.enabled", False)
driver_options.set_preference("useAutomationExtension", False)
user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) "\
"Gecko/20100101 Firefox/120.0"
driver_options.add_argument(f"user-agent={user_agent}")
# init browser driver
self.__driver_path = self.__system_config_reader.get("web_driver/driver_path")
self.__driver_type = self.__system_config_reader.get("web_driver/driver_type")
self.__driver_path = web_driver_config.get("driver_path")
if not self.__driver_path:
raise Exception(f"未配置浏览器驱动路径 !")
self.__driver_path = os.path.abspath(self.__driver_path)
try:
service = None
if self.__driver_path:
service = Service(executable_path=self.__driver_path)
match self.__driver_type.lower():
case "edge":
self.__driver = webdriver.Edge(service=service, options=edge_options)
service = EdgeService(executable_path=self.__driver_path)
self.__driver = webdriver.Edge(service=service, options=driver_options)
case "chrome":
self.__driver = webdriver.Chrome(service=service, options=edge_options)
service = ChromeService(executable_path=self.__driver_path)
self.__driver = webdriver.Chrome(service=service, options=driver_options)
case "firefox":
self.__driver = webdriver.Firefox(service=service, options=edge_options)
self._showTrace(f"Firefox 浏览器驱动初始化略慢, 请耐心等待...")
service = FirefoxService(executable_path=self.__driver_path)
self.__driver = webdriver.Firefox(service=service, options=driver_options)
case _:
raise Exception(f"不支持的浏览器驱动类型: {self.__driver_type}")
self.__driver.implicitly_wait(1)
@@ -114,6 +149,7 @@ class AutoLib(MsgBase):
self.__lib_logout = LibLogout(self._input_queue, self._output_queue, self.__driver)
self.__lib_reserve = LibReserve(self._input_queue, self._output_queue, self.__driver)
self.__lib_checkin = LibCheckin(self._input_queue, self._output_queue, self.__driver)
self.__lib_renew = LibRenew(self._input_queue, self._output_queue, self.__driver)
def __waitResponseLoad(
@@ -147,8 +183,11 @@ class AutoLib(MsgBase):
self,
) -> bool:
url = self.__system_config_reader.get("library/host_url")
url += self.__system_config_reader.get("library/login_url")
lib_config = self.__run_config.get("library", None)
if not lib_config:
self._showError("未配置图书馆参数 !")
return False
url = lib_config.get("host_url") + lib_config.get("login_url")
self.__driver.get(url)
if not self.__waitResponseLoad():
return False
@@ -159,24 +198,26 @@ class AutoLib(MsgBase):
self,
username: str,
password: str,
login_config: dict,
run_mode_config: dict,
reserve_info: dict
) -> int:
# result : 0 - success, 1 - failed, 2 - passed
# result : -1 - terminate, 0 - success, 1 - failed, 2 - passed
result = 2
# login
if not self.__lib_login.login(
username,
password,
self.__system_config_reader.get("login/max_attempt", 5),
self.__system_config_reader.get("login/auto_captcha", True),
login_config.get("max_attempt", 3),
login_config.get("auto_captcha", True),
):
return 1
"""
Here, we collect the run mode from the config file.
Here, we collect the run mode from the run config.
"""
run_mode = self.__system_config_reader.get("mode/run_mode", 0)
run_mode = run_mode_config.get("run_mode", 0)
run_mode = {
"auto_reserve": run_mode&0x1,
"auto_checkin": run_mode&0x2,
@@ -185,77 +226,79 @@ class AutoLib(MsgBase):
# reserve
if run_mode["auto_reserve"]:
if self.__lib_checker.canReserve(reserve_info.get("date")):
if self.__lib_reserve.reserve(reserve_info):
self._showTrace(f"用户 {username} 预约成功 !")
if self.__lib_reserve.reserve(username, reserve_info):
result = 0
else:
self._showTrace(f"用户 {username} 预约失败 !")
result = 1
else:
self._showTrace(f"用户 {username} 无法预约,已跳过")
result = 2
# checkin
if run_mode["auto_checkin"] and result == 2:
if self.__lib_checker.canCheckin(reserve_info.get("date")):
if self.__lib_checker.canCheckin():
if self.__lib_checkin.checkin(username):
self._showTrace(f"用户 {username} 签到成功 !")
result = 0
else:
self._showTrace(f"用户 {username} 签到失败 !")
result = 1
else:
self._showTrace(f"用户 {username} 无法签到,已跳过")
result = 2
# renewal
if run_mode["auto_renewal"] and result == 2:
if self.__lib_checker.canRenew(reserve_info.get("date")):
pass
if record := self.__lib_checker.canRenew():
if self.__lib_renew.renew(username, record, reserve_info):
if self.__lib_checker.postRenewCheck(record):
result = 0
else:
result = 1
else:
result = 1
else:
self._showTrace(f"用户 {username} 无法续约,已跳过")
result = 2
# logout
if not self.__lib_logout.logout(
username,
username
):
# if logout is failed, we must make sure the host to be reloaded
# otherwise, the next login may fail
self.__driver.get(self.__system_config_reader.get("library/host_url"))
return 1
if not self.__initDriverUrl():
return -1
return result
def run(
self,
system_config_reader: ConfigReader,
users_config_reader: ConfigReader
user_config: dict
):
self.__system_config_reader = system_config_reader
self.__users_config_reader = users_config_reader
if not self.__initBrowserDriver():
return
else:
if not self.__initDriverUrl():
return
self.__initLibOperators()
self.__user_config = user_config
user_counter = {"current": 0, "success": 0, "failed": 0, "passed": 0}
users = self.__users_config_reader.get("users")
self._showTrace(
f"共发现 {len(users)} 个用户, "\
f"用户配置文件路径: {self.__users_config_reader.configPath()}"
)
users = self.__user_config["users"]
self._showTrace(f"共发现 {len(users)} 个用户")
for user in users:
user_counter["current"] += 1
self._showTrace(
f"正在处理第 {user_counter["current"]}/{len(users)} 个用户: {user['username']}......"
f"正在处理第 {user_counter["current"]}/{len(users)} 个用户: {user["username"]}......"
)
if not user["enabled"]:
self._showTrace(f"用户 {user["username"]} 已跳过")
user_counter["passed"] += 1
continue
r = self.__run(
username=user["username"],
password=user["password"],
login_config=self.__run_config["login"],
run_mode_config=self.__run_config["mode"],
reserve_info=user["reserve_info"],
)
if r == 0:
if r == -1:
self._showTrace(
f"用户 {user["username"]} 处理过程中页面发生异常,无法继续操作, 任务已终止 !"
)
break
elif r == 0:
user_counter["success"] += 1
elif r == 1:
user_counter["failed"] += 1
@@ -274,6 +317,8 @@ class AutoLib(MsgBase):
) -> bool:
if self.__driver:
if self.__driver_type.lower() == "firefox":
self._showTrace(f"Firefox 浏览器驱动关闭略慢, 请耐心等待...")
self.__driver.quit()
self.__driver = None
self._showTrace(f"浏览器驱动已关闭")
+51 -10
View File
@@ -25,7 +25,7 @@ class LibChecker(LibOperator):
self,
input_queue: queue.Queue,
output_queue: queue.Queue,
driver
driver: any
):
super().__init__(input_queue, output_queue)
@@ -270,11 +270,11 @@ class LibChecker(LibOperator):
def canCheckin(
self,
date: str
self
) -> bool:
# have a reserved record in the given date
# only check the current date
date = time.strftime("%Y-%m-%d", time.localtime())
record = self.__getReserveRecord(date, "已预约")
if record is not None:
begin_time = record["time"]["begin"]
@@ -307,11 +307,11 @@ class LibChecker(LibOperator):
def canRenew(
self,
date: str
) -> bool:
self
):
# have a using record in the given date
# only check the current date
date = time.strftime("%Y-%m-%d", time.localtime())
record = self.__getReserveRecord(date, "使用中")
if record is not None:
end_time = record["time"]["end"]
@@ -325,9 +325,50 @@ class LibChecker(LibOperator):
)
if abs(time_diff_seconds) < 120*60:
self._showTrace(f"{trace_msg}, 可以续约")
return True
return record
else:
self._showTrace(f"{trace_msg}, 无法续约")
return False
return None
self._showTrace(f"用户在 {date} 没有有效预约记录, 无法续约")
return None
def postRenewCheck(
self,
record: dict
):
"""
Check if the renew operation is successful
Args:
record (dict): The expected record after renewal
Returns:
bool: True if the renew operation is successful, False otherwise
"""
# because the special circumstance that the renew operation
# do not show the success message or anything else,
# we need to check the record data to make sure the renew operation is successful.
# only check the given record date
date = record["date"]
act_record = self.__getReserveRecord(date, "使用中")
if act_record is not None:
if act_record["time"]["begin"] == record["time"]["begin"] and\
act_record["time"]["end"] == record["time"]["end"]:
self._showTrace(f"\n"\
f" 续约成功 !\n"\
f" 日 期 {date}\n"\
f" 时 间 {act_record["time"]["begin"]} - {act_record["time"]["end"]}\n"\
f" 位 置 {act_record["info"]["location"]}\n"
f" 状 态 {act_record["info"]["status"]}"
)
return True
else:
self._showTrace(f"\n"\
f" 续约失败 !\n"\
f" 续约后结束时间为 {act_record["time"]["end"]},与预期结束时间 {record["time"]["end"]} 不符 !"
)
return False
self._showTrace(f"用户在 {date} 没有有效预约记录, 无法检查续约结果")
return False
+16 -7
View File
@@ -25,7 +25,7 @@ class LibCheckin(LibOperator):
self,
input_queue: queue.Queue,
output_queue: queue.Queue,
driver
driver: any
):
super().__init__(input_queue, output_queue)
@@ -54,7 +54,6 @@ class LibCheckin(LibOperator):
except:
self._showTrace("签到时发生未知错误 !")
return False
print(result_message_element)
result_message = result_message_element.text
if "签到成功" in result_message:
try:
@@ -71,16 +70,21 @@ class LibCheckin(LibOperator):
f" {details[1]}\n"\
f" {details[2]}\n"\
f" {details[3]}\n"\
f" {details[4]}")
f" {details[4]}"
)
else:
self._showTrace(
self._showTrace(f"\n"\
" 签到成功 !\n"\
" 未获取到签到详情 !")
" 未获取到签到详情 !"
)
ok_btn.click()
return True
else:
failure_reason = result_message.replace("签到失败", "").strip()
self._showTrace(f"签到失败: {failure_reason}")
self._showTrace(f"\n"\
" 签到失败 !\n"\
f" {failure_reason}"
)
ok_btn.click()
return False
@@ -104,4 +108,9 @@ class LibCheckin(LibOperator):
self._showTrace("签到按钮不可用, 可能不在场馆内, 请连接图书馆网络后重试")
return False
checkin_btn.click()
return self._waitResponseLoad()
if self._waitResponseLoad():
self._showTrace(f"用户 {username} 签到成功 !")
return True
else:
self._showTrace(f"用户 {username} 签到失败 !")
return False
+1 -1
View File
@@ -25,7 +25,7 @@ class LibCheckout(LibOperator):
self,
input_queue: queue.Queue,
output_queue: queue.Queue,
driver
driver: any
):
super().__init__(input_queue, output_queue)
+11 -14
View File
@@ -26,7 +26,7 @@ class LibLogin(LibOperator):
self,
input_queue: queue.Queue,
output_queue: queue.Queue,
driver
driver: any
):
super().__init__(input_queue, output_queue)
@@ -88,13 +88,12 @@ class LibLogin(LibOperator):
captcha_img = base64.b64decode(base64_str)
captcha_text = self.__ddddocr.classification(captcha_img)
captcha_text = ''.join(filter(str.isalnum, captcha_text)).lower()
self._showTrace(f"识别到验证码为 : '{captcha_text}'.")
self._showTrace(f"识别到验证码为 : '{captcha_text}'")
if len(captcha_text) != 4:
raise Exception("识别到的验证码长度不等于 4 个字符 !")
return captcha_text
except Exception as e:
self._showTrace(f"验证码识别失败 ! : {e}")
self.__refreshCaptcha()
return ""
@@ -104,15 +103,14 @@ class LibLogin(LibOperator):
# manual recognize captcha
try:
self._show_msg("请输入验证码:")
captcha_text = self._wait_msg(timeout=15)
self._showTrace(f"输入的验证码为 : '{captcha_text}'.")
self._showMsg("请输入验证码:")
captcha_text = self._waitMsg(timeout=15)
self._showTrace(f"输入的验证码为 : '{captcha_text}'")
if len(captcha_text) != 4:
raise Exception("输入的验证码长度不等于 4 个字符 !")
return captcha_text
except Exception as e:
self._showTrace(f"输入验证码失败 ! : {e}")
self.__refreshCaptcha()
return ""
@@ -126,11 +124,9 @@ class LibLogin(LibOperator):
self.__driver.find_element(
By.ID, "loadImgId"
).click()
time.sleep(1)
return True
except Exception as e:
self._showTrace(f"刷新验证码失败 ! : {e}")
self.__refreshCaptcha()
return False
@@ -139,8 +135,7 @@ class LibLogin(LibOperator):
auto_captcha: bool = True
) -> str:
max_attempts = 5
max_attempts = 3 # the possibility of 3 times failed is less than (10%^3)
for _ in range(max_attempts):
if auto_captcha:
captcha_text = self.__autoRecognizeCaptcha()
@@ -149,7 +144,10 @@ class LibLogin(LibOperator):
captcha_text = self.__manualRecognizeCaptcha()
if captcha_text:
return captcha_text
self._showTrace(f"验证码识别失败 {max_attempts} 次, 请检查验证码是否正确 !")
else:
if not self.__refreshCaptcha():
return ""
self._showTrace(f"验证码识别失败 {max_attempts} 次, 达到最大尝试次数 !")
return ""
@@ -165,7 +163,6 @@ class LibLogin(LibOperator):
return True
except Exception as e:
self._showTrace(f"验证码填写失败 ! : {e}")
self.__refreshCaptcha()
return False
@@ -200,7 +197,7 @@ class LibLogin(LibOperator):
"//input[@type='button' and @value='登录']"
).click()
except Exception as e:
self._showTrace(f"登录失败 ! : {e}")
self._showTrace(f"尝试登录失败 ! : {e}")
continue
if self._waitResponseLoad():
self._showTrace(f"用户 {username}{attempt + 1} 次登录成功 !")
+1 -1
View File
@@ -22,7 +22,7 @@ class LibLogout(LibOperator):
self,
input_queue: queue.Queue,
output_queue: queue.Queue,
driver
driver: any
):
super().__init__(input_queue, output_queue)
+184 -2
View File
@@ -8,8 +8,14 @@ You may use, modify, and distribute this file under the terms of the MIT License
See the LICENSE file for details.
"""
import os
import time
import queue
from datetime import datetime, timedelta
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from base.LibOperator import LibOperator
@@ -19,7 +25,7 @@ class LibRenew(LibOperator):
self,
input_queue: queue.Queue,
output_queue: queue.Queue,
driver
driver: any
):
super().__init__(input_queue, output_queue)
@@ -31,4 +37,180 @@ class LibRenew(LibOperator):
self
) -> bool:
pass
self.__driver.refresh()
return True
@staticmethod
def __timeToMins(
time_str: str
) -> int:
hour, minute = map(int, time_str.split(":"))
return hour*60 + minute
@staticmethod
def __minsToTime(
mins: int
) -> str:
hour, minute = divmod(mins, 60)
return f"{hour:02d}:{minute:02d}"
def __waitRenewDialog(
self
) -> bool:
try:
WebDriverWait(self.__driver, 2).until(
EC.visibility_of_element_located((By.ID, "extendDiv"))
)
head_message = WebDriverWait(self.__driver, 2).until(
EC.presence_of_element_located((By.CSS_SELECTOR, "#extendDiv p.messageHead"))
)
result_message = WebDriverWait(self.__driver, 2).until(
EC.presence_of_element_located((By.CSS_SELECTOR, "#extendDiv div.resultMessage"))
)
except:
self._showTrace("续约时间选择界面加载失败 !")
return False
head_message = head_message.text.strip()
if "警告" in head_message:
result_message = result_message.text.strip()
self._showTrace(f"\n"\
f" 续约失败 !\n"\
f" {result_message}")
return False
try:
WebDriverWait(self.__driver, 2).until(
EC.presence_of_all_elements_located(
(By.CSS_SELECTOR, "#extendDiv .renewal_List li")
)
)
WebDriverWait(self.__driver, 2).until(
EC.presence_of_element_located((By.CSS_SELECTOR, "#extendDiv .btnOK"))
)
except:
self._showTrace("续约时间选择界面加载失败 !")
return False
return True
def __selectNearstTime(
self,
record: dict,
reserve_info: dict
) -> bool:
"""
TODO : this function is too long and too ugly
we need to refactor it to make it more readable.
but may be it is not a good idea to refactor it. :) who knows...
"""
end_time = record["time"]["end"]
renew_info = reserve_info["renew_time"]
max_diff = renew_info["max_diff"]
prefer_earlier = renew_info["prefer_early"]
target_renew_mins = self.__timeToMins(end_time) + renew_info["expect_duration"]*60
renew_ok_btn = self.__driver.find_element(
By.CSS_SELECTOR, "#extendDiv .btnOK"
)
try:
renew_time_opts = self.__driver.find_elements(
By.CSS_SELECTOR, "#extendDiv .renewal_List li"
)
free_times = []
best_time_diff = max_diff
best_actual_diff = None
best_time_opt = None
if not renew_time_opts:
self._showTrace("当前未查询到可用续约时间 !")
return False
for time_opt in renew_time_opts:
time_attr = time_opt.get_attribute("id")
if time_attr and time_attr.isdigit():
time_val = int(time_attr)
free_times.append(time_opt.text.strip())
else:
continue
actual_diff = time_val - target_renew_mins
abs_diff = abs(actual_diff)
if abs_diff < best_time_diff or (
abs_diff == best_time_diff and (
# 优先选择更早的时间
(prefer_earlier and actual_diff <= 0) or
# 优先选择更晚的时间
(not prefer_earlier and actual_diff >= 0)
)
):
best_time_diff = abs_diff
best_actual_diff = actual_diff
best_time_opt = time_opt
if best_time_opt is not None:
best_time_opt.click()
abs_time_diff = abs(best_actual_diff)
if best_actual_diff < 0:
time_relation = f"早了 {abs_time_diff} 分钟"
elif best_actual_diff > 0:
time_relation = f"晚了 {abs_time_diff} 分钟"
else:
time_relation = f"正好等于续约时间"
self._showTrace(
f"选择距离期望续约时间最近的 {best_time_opt.text}, "\
f"与期望续约时间相比 {time_relation}"
)
# update the actual renew end time
record["time"]["end"] = best_time_opt.text.strip()
renew_ok_btn.click()
return True
self._showTrace(
"无法选择最近的可用续约时间 !" \
f"所有可选时间与目标时间相差都超过了 {max_diff} 分钟 !"
)
self._showTrace(
f"当前可供续约的时间有: {free_times}"
)
return False
except:
self._showTrace("查询可用续约时间时发生未知错误 !")
return False
def renew(
self,
username: str,
record: dict,
reserve_info: dict
) -> bool:
if self.__driver is None:
self._showTrace("未提供有效 WebDriver 实例 !")
return False
try:
renew_btn = WebDriverWait(self.__driver, 2).until(
EC.element_to_be_clickable((By.ID, "btnExtend"))
)
except:
self._showTrace(f"用户 {username} 续约界面加载失败 !")
return False
if "disabled" in renew_btn.get_attribute("class"):
self._showTrace(f"用户 {username} 续约按钮不可用, 可能不在场馆内, 请连接图书馆网络后重试")
return False
renew_btn.click()
if not self.__waitRenewDialog():
self._showTrace(f"用户 {username} 续约失败 !")
# After the renewal, the webpage will display a mask overlay,
# so we need to refresh the page for subsequent operations.
self.__driver.refresh()
return False
if not self.__selectNearstTime(record, reserve_info):
self._showTrace(f"用户 {username} 续约失败 !")
self.__driver.refresh()
return False
if self._waitResponseLoad():
return True
+27 -12
View File
@@ -25,7 +25,7 @@ class LibReserve(LibOperator):
self,
input_queue: queue.Queue,
output_queue: queue.Queue,
driver
driver: any
):
super().__init__(input_queue, output_queue)
@@ -88,11 +88,13 @@ class LibReserve(LibOperator):
f" {contents[1]}\n"\
f" {contents[2]}\n"\
f" {contents[3]}\n"\
f" 签到时间 {contents[5]}")
f" 签到时间 {contents[5]}"
)
else:
self._showTrace(f"\n"\
f" 预约成功 !\n"\
f" 未找获取到详细信息")
self._showTrace("\n"\
" 预约成功 !\n"\
" 未找获取到详细信息"
)
return True
except:
self._showTrace(f"预约结果加载失败 !")
@@ -187,12 +189,13 @@ class LibReserve(LibOperator):
reserve_info: dict
) -> bool:
if reserve_info.get("expect_duration") is None:
reserve_info["expect_duration"] = 4
self._showTrace("预约持续时间未指定, 使用默认时长为 4 小时")
if reserve_info.get("satisfy_duration") is None:
reserve_info["satisfy_duration"] = True
self._showTrace("预约满足时长要求未指定, 默认满足")
if reserve_info["satisfy_duration"]:
if reserve_info.get("expect_duration") is None:
reserve_info["expect_duration"] = 4
self._showTrace("需要满足预约持续时间, 但未指定, 使用默认时长为 4 小时")
return True
@@ -234,7 +237,7 @@ class LibReserve(LibOperator):
# if end time is earlier than begin_time, exchange them
if end_mins < begin_mins:
self._showTrace(
f"结束时间 {end_time['time']} 早于开始时间 {begin_time['time']}, 自动交换"
f"结束时间 {end_time['time']} 早于开始时间 {begin_time['time']}, 尝试交换时间"
)
reserve_info["end_time"] = begin_time
reserve_info["begin_time"] = end_time
@@ -261,10 +264,9 @@ class LibReserve(LibOperator):
if end_mins - begin_mins > 8*60:
self._showTrace(
f"该用户未设置优先满足时长要求, 但是检查到预约持续时间 "
f"{int((end_mins - begin_mins)/60)} 小时 "
f"{float((end_mins - begin_mins)/60)} 小时 "
f"超出最大时长 8 小时, 自动设置为 8 小时"
)
reserve_info["expect_duration"] = 8
reserve_info["end_time"]["time"] = self.__minsToTime(begin_mins + 8*60)
return True
@@ -577,6 +579,7 @@ class LibReserve(LibOperator):
expect_begin_time = actual_begin_time = begin_time["time"]
expect_end_time = actual_end_time = end_time["time"]
expect_begin_mins = self.__timeToMins(expect_begin_time)
actual_begin_mins = expect_begin_mins
expect_end_mins = self.__timeToMins(expect_end_time)
# select the begin time
@@ -590,11 +593,18 @@ class LibReserve(LibOperator):
return False
else:
actual_begin_time = self.__minsToTime(expect_begin_mins)
actual_begin_mins = self.__timeToMins(actual_begin_time)
# if 'satisfy_duration' is True.
# select the end time based on the begin time
# (because it may be changed under the 'max time diff' strategy) and expect duration.
if satisfy_duration:
expect_end_mins = int(expect_begin_mins + expct_duration*60)
expect_end_mins = int(actual_begin_mins + expct_duration*60)
if expect_end_mins > self.__timeToMins("23:30"):
expect_end_mins = self.__timeToMins("23:30")
self._showTrace(
f"预约持续时间 {expct_duration} 小时, 超过最大预约时间 23:30, 自动调整为 23:30"
)
expect_end_time = self.__minsToTime(expect_end_mins)
self._showTrace(
f"需要满足期望预约持续时间: {expct_duration} 小时, "\
f"根据开始时间 {actual_begin_time} 计算结束时间: {self.__minsToTime(expect_end_mins)}"
@@ -619,6 +629,7 @@ class LibReserve(LibOperator):
def reserve(
self,
username: str,
reserve_info: dict
) -> bool:
@@ -674,4 +685,8 @@ class LibReserve(LibOperator):
self._showTrace(f"预约提交失败 !")
if not submit_reserve and have_hover_on_page:
self.__driver.refresh()
if reserve_success:
self._showTrace(f"用户 {username} 预约成功 !")
else:
self._showTrace(f"用户 {username} 预约失败 !")
return reserve_success