mirror of
https://github.com/KenanZhu/AutoLibrary.git
synced 2026-06-18 07:23:03 +08:00
Compare commits
83 Commits
v0.0.5-alpha
...
v1.0.2
| Author | SHA1 | Date | |
|---|---|---|---|
| 43b87db4eb | |||
| ae23f65e5a | |||
| a7b9c340ae | |||
| 96d733d2ed | |||
| 65cb951ada | |||
| 94ce3433a3 | |||
| dd48c8a01c | |||
| 924db3bdcc | |||
| 1e5452d411 | |||
| 1b378e5aaa | |||
| e069efb2ea | |||
| 407d25570a | |||
| bfcb65f56a | |||
| cde1e966e7 | |||
| 8c4f463889 | |||
| 39867cc20c | |||
| 149910d628 | |||
| 2a7ed099bf | |||
| 473f32ca29 | |||
| 580052f1e3 | |||
| 6abf530307 | |||
| 577c651ef8 | |||
| 18ae949900 | |||
| ca9059d1db | |||
| ad4deae0c6 | |||
| 55ae4d0d96 | |||
| 7dcd72939b | |||
| bfce61f4b4 | |||
| 60a5699822 | |||
| aab9565012 | |||
| 9255eec9f1 | |||
| cff6fd8fc0 | |||
| b129f47b48 | |||
| 069429be71 | |||
| 7d064fc8e7 | |||
| 1b172ad396 | |||
| 05c9d433f4 | |||
| 65ca40438d | |||
| 0a8763add5 | |||
| c5e589f3d1 | |||
| 5e5deba773 | |||
| 842fb434f4 | |||
| 6cabddf0cd | |||
| 0322558339 | |||
| 703ee527ae | |||
| 9a925fecb6 | |||
| 189fddfb6a | |||
| c2d53a8b78 | |||
| b99431476a | |||
| 977c0835b7 | |||
| cd565ec57d | |||
| 9f17474c1b | |||
| 04d66346dc | |||
| f858295af1 | |||
| cd6c899388 | |||
| 1038a86aff | |||
| 15ea47dd07 | |||
| 829a8440ad | |||
| 389ac885d3 | |||
| 68b61b5c8c | |||
| fd5abb5f1e | |||
| 1f16181aeb | |||
| f0c25903a3 | |||
| b24e4f473f | |||
| 8bb65be0b9 | |||
| 631785122b | |||
| 82ea40d3dc | |||
| 1244084c75 | |||
| 7a599c4f63 | |||
| d2cef258aa | |||
| a4c5ee299e | |||
| 7d92717136 | |||
| 3ef301f199 | |||
| b10d333eb2 | |||
| d5dc012ade | |||
| dac87068ef | |||
| f13d983124 | |||
| 30db2fbf9a | |||
| 11bcd77208 | |||
| 833e5576da | |||
| efa211761d | |||
| 269eed1cac | |||
| 99975ebeee |
@@ -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 }}
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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
|
||||
+21
-14
@@ -1,14 +1,21 @@
|
||||
**/build
|
||||
**/dist
|
||||
**/models/*.onnx
|
||||
**/driver/*.exe
|
||||
**/.git
|
||||
**/.vscode
|
||||
**/.stfolder
|
||||
**/.stignore
|
||||
**/gui/AutoLibraryResources.py
|
||||
**/__pycache__
|
||||
**/gui/AutoLibraryResource.py
|
||||
**/gui/Ui_ALMainWindow.py
|
||||
**/gui/Ui_ALConfigWidget.py
|
||||
**/gui/translators/qtbase_zh_CN.qm
|
||||
.vscode/
|
||||
|
||||
.stfolder
|
||||
.stignore
|
||||
|
||||
__pycache__/
|
||||
build/
|
||||
dist/
|
||||
model/*.onnx
|
||||
driver/*.exe
|
||||
src/gui/configs/*.json
|
||||
src/gui/translators/qtbase_zh_CN.qm
|
||||
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
|
||||
|
||||
-233
@@ -1,233 +0,0 @@
|
||||
# -*- 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 queue
|
||||
|
||||
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 MsgBase import MsgBase
|
||||
from LibLogin import LibLogin
|
||||
from LibLogout import LibLogout
|
||||
from LibReserve import LibReserve
|
||||
|
||||
from ConfigReader import ConfigReader
|
||||
|
||||
|
||||
class AutoLib(MsgBase):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
input_queue: queue.Queue,
|
||||
output_queue: queue.Queue,
|
||||
):
|
||||
super().__init__(input_queue, output_queue)
|
||||
|
||||
self.__system_config_reader = None
|
||||
self.__users_config_reader = None
|
||||
self.__driver = None
|
||||
|
||||
|
||||
def __initBrowserDriver(
|
||||
self
|
||||
) -> 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")
|
||||
|
||||
edge_options.add_argument("--window-size=1280,720")
|
||||
edge_options.add_argument("--remote-allow-origins=*")
|
||||
|
||||
# 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")
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
# 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 = 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)
|
||||
case "chrome":
|
||||
self.__driver = webdriver.Chrome(service=service, options=edge_options)
|
||||
case "firefox":
|
||||
self.__driver = webdriver.Firefox(service=service, options=edge_options)
|
||||
case _:
|
||||
raise Exception(f"不支持的浏览器驱动类型: {self.__driver_type}")
|
||||
self.__driver.implicitly_wait(10)
|
||||
self.__driver.execute_script(
|
||||
"Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
|
||||
)
|
||||
except Exception as e:
|
||||
self._showTrace(f"浏览器驱动初始化失败: {e}")
|
||||
return False
|
||||
# init library operators
|
||||
self.__lib_login = LibLogin(self._input_queue, self._output_queue, self.__driver)
|
||||
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._showTrace(f"浏览器驱动已初始化, 类型: {self.__driver_type}, 路径: {self.__driver_path}")
|
||||
return True
|
||||
|
||||
|
||||
def __waitResponseLoad(
|
||||
self,
|
||||
) -> bool:
|
||||
|
||||
# wait for page load
|
||||
try:
|
||||
WebDriverWait(self.__driver, 5).until( # title contains "首页"
|
||||
EC.title_contains("首页")
|
||||
)
|
||||
WebDriverWait(self.__driver, 2).until( # username field presence
|
||||
EC.presence_of_element_located((By.NAME, "username"))
|
||||
)
|
||||
WebDriverWait(self.__driver, 2).until( # password field presence
|
||||
EC.presence_of_element_located((By.NAME, "password"))
|
||||
)
|
||||
WebDriverWait(self.__driver, 2).until( # captcha field presence
|
||||
EC.presence_of_element_located((By.NAME, "answer"))
|
||||
)
|
||||
WebDriverWait(self.__driver, 2).until( # captcha image presence
|
||||
EC.presence_of_element_located((By.ID, "loadImgId"))
|
||||
)
|
||||
return True
|
||||
except:
|
||||
self._showTrace(f"登录页面加载失败 !")
|
||||
return False
|
||||
|
||||
|
||||
def __initDriverUrl(
|
||||
self,
|
||||
) -> bool:
|
||||
|
||||
self.__driver.get(self.__system_config_reader.get("library/host_url"))
|
||||
if not self.__waitResponseLoad():
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def __run(
|
||||
self,
|
||||
username: str,
|
||||
password: str,
|
||||
reserve_info: dict,
|
||||
) -> bool:
|
||||
|
||||
success = False
|
||||
|
||||
# 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),
|
||||
):
|
||||
return False
|
||||
run_mode = self.__system_config_reader.get("run/mode", 1)
|
||||
run_mode = {
|
||||
"auto_reserve": run_mode&0x1,
|
||||
"auto_checkin": run_mode&0x2,
|
||||
"auto_renewal": run_mode&0x4,
|
||||
}
|
||||
# reserve or checkin or renewal
|
||||
"""
|
||||
Here, we collect the run mode from the config file.
|
||||
"""
|
||||
if self.__lib_reserve.canReserve(reserve_info.get("date")) and run_mode["auto_reserve"]:
|
||||
if self.__lib_reserve.reserve(reserve_info):
|
||||
self._showTrace(f"用户 {username} 预约成功 !")
|
||||
success = True
|
||||
else:
|
||||
self._showTrace(f"用户 {username} 预约失败 !")
|
||||
success = False
|
||||
# logout
|
||||
if not self.__lib_logout.logout(
|
||||
username,
|
||||
):
|
||||
self.__driver.get(self.__system_config_reader.get("library/host_url"))
|
||||
return False
|
||||
return success
|
||||
|
||||
|
||||
def run(
|
||||
self,
|
||||
system_config_reader: ConfigReader,
|
||||
users_config_reader: ConfigReader,
|
||||
):
|
||||
|
||||
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
|
||||
|
||||
user_counter = {"current": 0, "success": 0, "failed": 0}
|
||||
users = self.__users_config_reader.get("users")
|
||||
self._showTrace(f"共发现 {len(users)} 个用户, "\
|
||||
f"用户配置文件路径: {self.__users_config_reader.configPath()}")
|
||||
|
||||
for user in users:
|
||||
user_counter["current"] += 1
|
||||
self._showTrace(f"正在处理第 {user_counter["current"]}/{len(users)} 个用户: {user['username']}......")
|
||||
if self.__run(
|
||||
username=user["username"],
|
||||
password=user["password"],
|
||||
reserve_info=user["reserve_info"],
|
||||
):
|
||||
user_counter["success"] += 1
|
||||
else:
|
||||
user_counter["failed"] += 1
|
||||
self._showTrace(f"处理完成, 共计 {user_counter["current"]} 个用户, "\
|
||||
f"成功 {user_counter["success"]} 个用户, "\
|
||||
f"失败 {user_counter["failed"]} 个用户")
|
||||
return
|
||||
|
||||
|
||||
def close(
|
||||
self,
|
||||
) -> bool:
|
||||
|
||||
if self.__driver:
|
||||
self.__driver.quit()
|
||||
self.__driver = None
|
||||
self._showTrace(f"浏览器驱动已关闭")
|
||||
return True
|
||||
else:
|
||||
self._showTrace(f"浏览器驱动未初始化,无需关闭")
|
||||
return False
|
||||
@@ -1,40 +0,0 @@
|
||||
# -*- 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 re
|
||||
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 LibOperator import LibOperator
|
||||
|
||||
|
||||
class LibCheckin(LibOperator):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
input_queue: queue.Queue,
|
||||
output_queue: queue.Queue,
|
||||
driver
|
||||
):
|
||||
|
||||
super().__init__(input_queue, output_queue)
|
||||
|
||||
self.__driver = driver
|
||||
|
||||
|
||||
def _waitResponseLoad(
|
||||
self,
|
||||
) -> bool:
|
||||
|
||||
pass
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
# -*- 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 queue
|
||||
|
||||
from LibOperator import LibOperator
|
||||
|
||||
|
||||
class LibRenew(LibOperator):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
input_queue: queue.Queue,
|
||||
output_queue: queue.Queue,
|
||||
driver
|
||||
):
|
||||
|
||||
super().__init__(input_queue, output_queue)
|
||||
|
||||
self.__driver = driver
|
||||
|
||||
|
||||
def _waitResponseLoad(
|
||||
self,
|
||||
) -> bool:
|
||||
|
||||
pass
|
||||
@@ -1,847 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AutoLibrary 操作手册</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
|
||||
}
|
||||
|
||||
:root {
|
||||
--primary: #2c3e50;
|
||||
--secondary: #3498db;
|
||||
--accent: #e74c3c;
|
||||
--light: #f8f9fa;
|
||||
--dark: #2c3e50;
|
||||
--gray: #6c757d;
|
||||
--border: #dee2e6;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #c0c0c0a4;
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.manual-container {
|
||||
display: flex;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 280px;
|
||||
background: var(--primary);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
padding: 2rem 1rem;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding-bottom: 1.5rem;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
margin-bottom: 1.5rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.sidebar-header:hover {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.sidebar h1 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.sidebar-nav li {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.sidebar-nav a {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
padding: 0.7rem 1rem;
|
||||
border-radius: 5px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.sidebar-nav a:hover,
|
||||
.sidebar-nav a.active {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
background: rgb(245, 245, 245);
|
||||
padding: 2rem 3rem;
|
||||
overflow-y: auto;
|
||||
max-height: 100vh;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 2rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: var(--primary);
|
||||
margin-bottom: 1.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 2px solid var(--secondary);
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: var(--dark);
|
||||
margin: 1.5rem 0 1rem;
|
||||
}
|
||||
|
||||
.intro-box {
|
||||
background: #e3f2fd;
|
||||
border-left: 4px solid var(--secondary);
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
border-radius: 0 5px 5px 0;
|
||||
}
|
||||
|
||||
.step-container {
|
||||
counter-reset: step-counter;
|
||||
}
|
||||
|
||||
.step {
|
||||
display: flex;
|
||||
margin-bottom: 2rem;
|
||||
background: var(--light);
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.step-number {
|
||||
counter-increment: step-counter;
|
||||
background: var(--secondary);
|
||||
color: white;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
margin-right: 1.5rem;
|
||||
flex-shrink: 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.step-number::before {
|
||||
content: counter(step-counter);
|
||||
}
|
||||
|
||||
.step-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.step-image {
|
||||
background: #e9ecef;
|
||||
border-radius: 5px;
|
||||
height: 180px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--gray);
|
||||
margin: 1rem 0;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.note {
|
||||
background: #fff3cd;
|
||||
border-left: 4px solid #ffc107;
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
border-radius: 0 5px 5px 0;
|
||||
}
|
||||
|
||||
.warning {
|
||||
background: #f8d7da;
|
||||
border-left: 4px solid #dc3545;
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
border-radius: 0 5px 5px 0;
|
||||
}
|
||||
|
||||
.code-block {
|
||||
background: #2d2d2d;
|
||||
color: #f8f8f2;
|
||||
border: 1px solid #444;
|
||||
border-left: 4px solid var(--secondary);
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
font-family: 'Consolas', monospace;
|
||||
white-space: pre-wrap;
|
||||
border-radius: 0 5px 5px 0;
|
||||
font-size: 0.9rem;
|
||||
overflow-x: auto;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.code-block .key { color: #2b997f; }
|
||||
.code-block .string { color: #9acf65; }
|
||||
.code-block .number { color: #3dd942; }
|
||||
.code-block .boolean { color: #24e9ff; }
|
||||
.code-block .null { color: #ff79c6; }
|
||||
.code-block .property { color: #8be9fd; }
|
||||
.code-block .punctuation { color: #f8f8f2; }
|
||||
|
||||
.feature-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
background: white;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
font-size: 2rem;
|
||||
color: var(--secondary);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.config-tabs {
|
||||
display: flex;
|
||||
margin-bottom: 1rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.config-tab {
|
||||
padding: 0.8rem 1.5rem;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
background: none;
|
||||
font-size: 1rem;
|
||||
color: var(--gray);
|
||||
border-bottom: 3px solid transparent;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.config-tab.active {
|
||||
color: var(--secondary);
|
||||
border-bottom: 3px solid var(--secondary);
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
background: white;
|
||||
border-radius: 0 5px 5px 5px;
|
||||
padding: 1.5rem;
|
||||
border: 1px solid var(--border);
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.tab-pane {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-pane.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.download-section {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
background: var(--light);
|
||||
border-radius: 8px;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
background: var(--secondary);
|
||||
color: white;
|
||||
padding: 0.8rem 1.5rem;
|
||||
border-radius: 3px;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
transition: all 0.3s ease;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: #2980b9;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.faq-item {
|
||||
margin-bottom: 1.5rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.faq-question {
|
||||
padding: 1rem 1.5rem;
|
||||
background: var(--light);
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.faq-answer {
|
||||
padding: 1rem 1.5rem;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.faq-item.active .faq-answer {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.browser-drivers {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1.5rem;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.browser-card {
|
||||
flex: 1;
|
||||
background: white;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
text-align: center;
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.browser-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.browser-logo {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin: 0 auto 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.browser-logo img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.browser-card h4 {
|
||||
margin-bottom: 1rem;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.browser-card .btn {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.manual-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.feature-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="manual-container">
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h1>AutoLibrary</h1>
|
||||
<p>操作手册 v0.01</p>
|
||||
</div>
|
||||
<ul class="sidebar-nav">
|
||||
<li><a href="#intro" class="active">工具简介</a></li>
|
||||
<li><a href="#preparation">准备工作</a></li>
|
||||
<li><a href="#usage">使用步骤</a></li>
|
||||
<li><a href="#configuration">配置说明</a></li>
|
||||
<li><a href="#features">功能详解</a></li>
|
||||
<li><a href="#interface">用户界面</a></li>
|
||||
<li><a href="#troubleshooting">故障排除</a></li>
|
||||
<li><a href="#faq">常见问题</a></li>
|
||||
<li><a href="#download">下载安装</a></li>
|
||||
</ul>
|
||||
</aside>
|
||||
|
||||
<main class="content">
|
||||
<section id="name" class="section">
|
||||
<h1>AutoLibrary</h1>
|
||||
</section>
|
||||
|
||||
<section id="intro" class="section">
|
||||
<h2>工具简介</h2>
|
||||
<div class="step">
|
||||
<div class="step-content">
|
||||
<div class="intro-box">
|
||||
<p>AutoLibrary 是一款专为北京建筑大学图书馆设计的自动化工具,旨在帮助学生简化图书馆座位操作流程,节省宝贵时间。</p>
|
||||
</div>
|
||||
<p>本工具模拟人工操作,通过简单的界面交互使用。</p>
|
||||
|
||||
<h3>工具特点</h3>
|
||||
<ul>
|
||||
<p>模拟人工操作,不干扰图书馆系统正常运行</p>
|
||||
<p>支持多种预约模式,满足不同使用场景</p>
|
||||
<p>支持多账号批量预约</p>
|
||||
<p>自动处理验证码,减少人工干预</p>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="preparation" class="section">
|
||||
<h2>准备工作</h2>
|
||||
<div class="step-container">
|
||||
<div class="step">
|
||||
<div class="step-number"></div>
|
||||
<div class="step-content">
|
||||
<h3>下载浏览器驱动</h3>
|
||||
<p>工具需要通过浏览器驱动来控制浏览器,请根据您使用的浏览器下载对应版本的驱动:</p>
|
||||
|
||||
<div class="browser-drivers">
|
||||
<div class="browser-card">
|
||||
<div class="browser-logo">
|
||||
<img src="https://edgestatic.azureedge.net/welcome/static/favicon.png" alt="Microsoft Edge">
|
||||
</div>
|
||||
<h4>Microsoft Edge</h4>
|
||||
<p>适用于Windows 10/11系统</p>
|
||||
<a href="https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/" target="_blank" class="btn">下载驱动</a>
|
||||
</div>
|
||||
|
||||
<div class="browser-card">
|
||||
<div class="browser-logo">
|
||||
<img src="https://www.gstatic.cn/devrel-devsite/prod/v154b6c17f7870ab2939b3d571919274f806798dc59971188e1f4183601ea7775/chrome/images/touchicon-180.png" alt="Google Chrome">
|
||||
</div>
|
||||
<h4>Google Chrome</h4>
|
||||
<p>最常用的浏览器</p>
|
||||
<a href="https://developer.chrome.google.cn/docs/chromedriver/downloads" target="_blank" class="btn">下载驱动</a>
|
||||
</div>
|
||||
|
||||
<div class="browser-card">
|
||||
<div class="browser-logo">
|
||||
<img src="https://www.firefox.com/media/img/favicons/firefox/browser/favicon-196x196.59e3822720be.png" alt="Mozilla Firefox">
|
||||
</div>
|
||||
<h4>Mozilla Firefox</h4>
|
||||
<p>开源浏览器</p>
|
||||
<a href="https://github.com/mozilla/geckodriver/releases" target="_blank" class="btn">下载驱动</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="note">
|
||||
<strong>注意:</strong> 浏览器驱动版本必须与您的浏览器版本兼容,否则工具无法正常工作。
|
||||
</div>
|
||||
<div class="step-image">
|
||||
浏览器驱动下载页面截图区域
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step">
|
||||
<div class="step-number"></div>
|
||||
<div class="step-content">
|
||||
<h3>确认驱动路径</h3>
|
||||
<p>下载驱动后,将浏览器驱动程序的路径(如'C:\Users\Administrator\Downloads\msedgedriver.exe')输入到AutoLibrary配置界面中。</p>
|
||||
<div class="step-image">
|
||||
驱动文件放置位置截图区域
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="usage" class="section">
|
||||
<h2>使用步骤</h2>
|
||||
<div class="step-container">
|
||||
<div class="step">
|
||||
<div class="step-number"></div>
|
||||
<div class="step-content">
|
||||
<h3>编辑配置文件</h3>
|
||||
<p>使用文本编辑器(如记事本、Visual Studio Code等)打开config.json和users.json文件,按照您的需求修改参数。</p>
|
||||
<div class="warning">
|
||||
<strong>重要:</strong> 请勿使用Microsoft Word等富文本编辑器,这可能导致文件格式错误。
|
||||
</div>
|
||||
<div class="step-image">
|
||||
配置文件编辑截图区域
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step">
|
||||
<div class="step-number"></div>
|
||||
<div class="step-content">
|
||||
<h3>运行工具</h3>
|
||||
<p>双击运行main.exe文件,工具将自动开始预约流程。</p>
|
||||
<div class="step-image">
|
||||
工具运行界面截图区域
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step">
|
||||
<div class="step-number"></div>
|
||||
<div class="step-content">
|
||||
<h3>监控运行状态</h3>
|
||||
<p>如果headless模式设置为false,您将看到浏览器窗口自动操作。请勿手动干预浏览器窗口。</p>
|
||||
<div class="step-image">
|
||||
浏览器自动操作截图区域
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step">
|
||||
<div class="step-number"></div>
|
||||
<div class="step-content">
|
||||
<h3>查看结果</h3>
|
||||
<p>工具运行完成后,查看生成的日志文件确认预约结果。</p>
|
||||
<div class="step-image">
|
||||
运行日志截图区域
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="configuration" class="section">
|
||||
<h2>配置说明</h2>
|
||||
<p>AutoLibrary通过两个配置文件来控制工具行为:config.json(工具设置)和users.json(用户信息)。</p>
|
||||
|
||||
<div class="config-tabs">
|
||||
<button class="config-tab active" data-tab="config-tab">config.json</button>
|
||||
<button class="config-tab" data-tab="users-tab">users.json</button>
|
||||
</div>
|
||||
|
||||
<div class="tab-content">
|
||||
<div id="config-tab" class="tab-pane active">
|
||||
<h3>工具配置文件</h3>
|
||||
<p>config.json文件控制工具的基本运行参数:</p>
|
||||
<div class="code-block">
|
||||
{
|
||||
<span class="property">"library"</span>: {
|
||||
<span class="property">"lib_host_url"</span>: <span class="string">"http://10.1.20.7"</span>,
|
||||
<span class="property">"lib_login_url"</span>: <span class="string">"/login"</span>
|
||||
},
|
||||
<span class="property">"mode"</span>: {
|
||||
<span class="property">"run_mode"</span>: <span class="number">1</span>
|
||||
},
|
||||
<span class="property">"login"</span>: {
|
||||
<span class="property">"auto_captcha"</span>: <span class="key">true</span>,
|
||||
<span class="property">"login_attempt"</span>: <span class="number">3</span>
|
||||
},
|
||||
<span class="property">"web_driver"</span>: {
|
||||
<span class="property">"driver_type"</span>: <span class="string">"edge"</span>,
|
||||
<span class="property">"driver_path"</span>: <span class="string">"msedgedriver.exe"</span>,
|
||||
<span class="property">"headless"</span>: <span class="key">false</span>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<h4>参数说明</h4>
|
||||
<ul>
|
||||
<li><strong>run_mode</strong>: 运行模式,可组合使用(1+4+8=13)</li>
|
||||
<li><strong>auto_captcha</strong>: 自动验证码识别,建议保持true</li>
|
||||
<li><strong>login_attempt</strong>: 登录尝试次数,默认3次</li>
|
||||
<li><strong>driver_type</strong>: 浏览器类型(edge/chrome/firefox)</li>
|
||||
<li><strong>driver_path</strong>: 驱动文件路径</li>
|
||||
<li><strong>headless</strong>: 无头模式,false会显示浏览器窗口</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div id="users-tab" class="tab-pane">
|
||||
<h3>用户配置文件</h3>
|
||||
<p>users.json文件包含用户账号和预约信息:</p>
|
||||
<div class="code-block">
|
||||
{
|
||||
<span class="property">"users"</span>: [
|
||||
{
|
||||
<span class="property">"username"</span>: <span class="string">"您的学号"</span>,
|
||||
<span class="property">"password"</span>: <span class="string">"您的密码"</span>,
|
||||
<span class="property">"reserve_info"</span>: {
|
||||
<span class="property">"date"</span>: <span class="string">"2025-10-30"</span>,
|
||||
<span class="property">"start_time"</span>: <span class="string">"09:30"</span>,
|
||||
<span class="property">"end_time"</span>: <span class="string">"17:00"</span>,
|
||||
<span class="property">"place"</span>: <span class="string">"1"</span>,
|
||||
<span class="property">"floor"</span>: <span class="string">"4"</span>,
|
||||
<span class="property">"room"</span>: <span class="string">"5"</span>,
|
||||
<span class="property">"seat_id"</span>: <span class="string">"31A"</span>,
|
||||
<span class="property">"expect_duration"</span>: <span class="number">6</span>
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
</div>
|
||||
|
||||
<h4>参数说明</h4>
|
||||
<ul>
|
||||
<li><strong>username</strong>: 学号</li>
|
||||
<li><strong>password</strong>: 密码</li>
|
||||
<li><strong>date</strong>: 预约日期(格式:YYYY-MM-DD)</li>
|
||||
<li><strong>start_time/end_time</strong>: 预约时间段</li>
|
||||
<li><strong>place/floor/room</strong>: 图书馆位置信息</li>
|
||||
<li><strong>seat_id</strong>: 座位编号(重要)</li>
|
||||
<li><strong>expect_duration</strong>: 期望使用时长(小时)</li>
|
||||
</ul>
|
||||
<div class="note">
|
||||
<strong>提示:</strong> 可以添加多个用户,工具会按顺序处理每个用户的预约请求。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="features" class="section">
|
||||
<h2>功能详解</h2>
|
||||
<div class="feature-grid">
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">⏰</div>
|
||||
<h3>自动预约(模式 +1)</h3>
|
||||
<p>当您当前没有有效预约时,工具会自动为您预约指定座位。</p>
|
||||
<div class="note">
|
||||
<strong>适用场景:</strong> 提前预约第二天的座位
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">✅</div>
|
||||
<h3>自动签到(模式 +4)</h3>
|
||||
<p>如果您已有预约,且在可签到时间范围内,工具会自动完成签到。</p>
|
||||
<div class="note">
|
||||
<strong>适用场景:</strong> 避免因忘记签到而失去座位
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">🔄</div>
|
||||
<h3>自动续约(模式 +8)</h3>
|
||||
<p>当您正在使用座位且到达可续约时间时,工具会自动延长使用时间。</p>
|
||||
<div class="note">
|
||||
<strong>适用场景:</strong> 需要长时间使用座位的情况
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>模式组合使用</h3>
|
||||
<p>运行模式可以组合使用,只需将对应模式的数值相加:</p>
|
||||
<ul>
|
||||
<li>自动预约 + 自动签到 + 自动续约 = 13(推荐)</li>
|
||||
<li>自动预约 = 1</li>
|
||||
<li>自动预约 + 自动签到 = 5</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section id="troubleshooting" class="section">
|
||||
<h2>故障排除</h2>
|
||||
<h3>常见问题及解决方法</h3>
|
||||
|
||||
<div class="faq-item">
|
||||
<div class="faq-question">工具启动时报错"无法找到驱动"</div>
|
||||
<div class="faq-answer">
|
||||
<p>这是因为浏览器驱动未正确安装或版本不匹配。</p>
|
||||
<ul>
|
||||
<li>检查驱动文件是否放置在正确位置</li>
|
||||
<li>确认驱动版本与浏览器版本完全匹配</li>
|
||||
<li>尝试重新下载并安装驱动</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="faq-item">
|
||||
<div class="faq-question">登录失败,提示账号密码错误</div>
|
||||
<div class="faq-answer">
|
||||
<p>请检查users.json文件中的账号密码是否正确。</p>
|
||||
<ul>
|
||||
<li>确认学号和密码无误</li>
|
||||
<li>检查是否有特殊字符需要转义</li>
|
||||
<li>尝试手动登录图书馆系统确认账号可用</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="faq-item">
|
||||
<div class="faq-question">预约失败,提示座位不可用</div>
|
||||
<div class="faq-answer">
|
||||
<p>目标座位可能已被他人预约或不在可预约时间。</p>
|
||||
<ul>
|
||||
<li>确认座位编号是否正确</li>
|
||||
<li>检查预约时间是否符合图书馆规定</li>
|
||||
<li>尝试预约其他座位或调整预约时间</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="faq" class="section">
|
||||
<h2>常见问题</h2>
|
||||
|
||||
<div class="faq-item">
|
||||
<div class="faq-question">使用AutoLibrary是否安全?</div>
|
||||
<div class="faq-answer">
|
||||
<p>AutoLibrary完全模拟人工操作,不干扰图书馆系统正常运行。工具不会收集或上传您的个人信息,所有数据仅保存在本地配置文件中。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="faq-item">
|
||||
<div class="faq-question">可以同时预约多个座位吗?</div>
|
||||
<div class="faq-answer">
|
||||
<p>根据图书馆规定,每个账号同一时间段只能预约一个座位。但您可以在users.json中添加多个账号,工具会依次处理每个账号的预约请求。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="faq-item">
|
||||
<div class="faq-question">工具运行期间可以操作电脑吗?</div>
|
||||
<div class="faq-answer">
|
||||
<p>可以正常使用电脑,但请勿操作工具自动打开的浏览器窗口,否则可能会干扰工具的正常运行。</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="download" class="section">
|
||||
<h2>下载安装</h2>
|
||||
<div class="download-section">
|
||||
<h3>获取AutoLibrary</h3>
|
||||
<p>点击下方按钮下载最新版本的AutoLibrary工具包:</p>
|
||||
<a href="#" class="btn">下载 AutoLibrary v0.01</a>
|
||||
<div class="note" style="margin-top: 1.5rem;">
|
||||
<p>文件大小:约15MB</p>
|
||||
<p>系统要求:Windows 10/11,支持Edge/Chrome/Firefox浏览器</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>安装步骤</h3>
|
||||
<ol>
|
||||
<li>下载压缩包并解压到任意文件夹</li>
|
||||
<li>根据您使用的浏览器下载对应版本的驱动</li>
|
||||
<li>将驱动文件放置到工具文件夹中</li>
|
||||
<li>按照本手册说明编辑配置文件</li>
|
||||
<li>双击main.exe运行工具</li>
|
||||
</ol>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.querySelectorAll('.sidebar-nav a').forEach(link => {
|
||||
link.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
document.querySelectorAll('.sidebar-nav a').forEach(a => {
|
||||
a.classList.remove('active');
|
||||
});
|
||||
this.classList.add('active');
|
||||
|
||||
const targetId = this.getAttribute('href');
|
||||
const targetSection = document.querySelector(targetId);
|
||||
|
||||
if (targetSection) {
|
||||
targetSection.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.config-tab').forEach(tab => {
|
||||
tab.addEventListener('click', function() {
|
||||
const tabId = this.getAttribute('data-tab');
|
||||
|
||||
document.querySelectorAll('.config-tab').forEach(t => {
|
||||
t.classList.remove('active');
|
||||
});
|
||||
this.classList.add('active');
|
||||
|
||||
document.querySelectorAll('.tab-pane').forEach(pane => {
|
||||
pane.classList.remove('active');
|
||||
});
|
||||
|
||||
document.getElementById(tabId).classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.faq-question').forEach(question => {
|
||||
question.addEventListener('click', function() {
|
||||
const faqItem = this.parentElement;
|
||||
faqItem.classList.toggle('active');
|
||||
});
|
||||
});
|
||||
|
||||
const sections = document.querySelectorAll('.section');
|
||||
const navLinks = document.querySelectorAll('.sidebar-nav a');
|
||||
|
||||
const observerOptions = {
|
||||
root: null,
|
||||
rootMargin: '0px 0px -50% 0px',
|
||||
threshold: 0.2
|
||||
};
|
||||
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
const id = entry.target.getAttribute('id');
|
||||
|
||||
navLinks.forEach(link => {
|
||||
link.classList.remove('active');
|
||||
});
|
||||
|
||||
const activeLink = document.querySelector(`.sidebar-nav a[href="#${id}"]`);
|
||||
if (activeLink) {
|
||||
activeLink.classList.add('active');
|
||||
}
|
||||
}
|
||||
});
|
||||
}, observerOptions);
|
||||
|
||||
sections.forEach(section => {
|
||||
observer.observe(section);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
For more infomation, please visit our website: https://www.autolibrary.cv
|
||||
+1
-1
@@ -1 +1 @@
|
||||
This folder is used to store the browser driver by selenium.
|
||||
This folder is used to store the browser driver using by selenium.
|
||||
@@ -1,800 +0,0 @@
|
||||
# -*- 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
|
||||
|
||||
from PySide6.QtCore import (
|
||||
Qt, Signal, Slot, QTime, QDate, QDir, QFileInfo
|
||||
)
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget, QLineEdit, QMessageBox, QFileDialog, QListWidgetItem
|
||||
)
|
||||
from PySide6.QtGui import QCloseEvent
|
||||
|
||||
from .Ui_ALConfigWidget import Ui_ALConfigWidget
|
||||
|
||||
from ConfigReader import ConfigReader
|
||||
from ConfigWriter import ConfigWriter
|
||||
|
||||
|
||||
class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
|
||||
configWidgetCloseSingal = Signal(dict)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent = None,
|
||||
config_paths = {
|
||||
"system":
|
||||
f"{QDir.toNativeSeparators(QFileInfo(sys.executable).absoluteDir().absoluteFilePath("system.json"))}",
|
||||
"users":
|
||||
f"{QDir.toNativeSeparators(QFileInfo(sys.executable).absoluteDir().absoluteFilePath("users.json"))}",
|
||||
}
|
||||
):
|
||||
|
||||
super().__init__(parent)
|
||||
|
||||
self.setupUi(self)
|
||||
self.connectSignals()
|
||||
self.modifyUi()
|
||||
self.__config_paths = config_paths
|
||||
self.__system_config_data = self.loadSystemConfig(self.__config_paths["system"])
|
||||
self.__users_config_data = self.loadUsersConfig(self.__config_paths["users"])
|
||||
if not self.__system_config_data:
|
||||
self.initlizeDefaultConfig("system")
|
||||
if not self.__users_config_data:
|
||||
self.initlizeDefaultConfig("users")
|
||||
self.initlizeConfigToWidget("system", self.__system_config_data)
|
||||
self.initlizeConfigToWidget("users", self.__users_config_data)
|
||||
|
||||
|
||||
def modifyUi(
|
||||
self
|
||||
):
|
||||
|
||||
self.initlizeFloorRoomMap()
|
||||
self.initilizeUserInfoWidget()
|
||||
|
||||
|
||||
def connectSignals(
|
||||
self
|
||||
):
|
||||
|
||||
self.ShowPasswordCheckBox.clicked.connect(self.onShowPasswordCheckBoxChecked)
|
||||
self.FloorComboBox.currentIndexChanged.connect(self.onFloorComboBoxCurrentIndexChanged)
|
||||
self.UserListWidget.currentItemChanged.connect(self.onUserListWidgetCurrentItemChanged)
|
||||
self.AddUserButton.clicked.connect(self.onAddUserButtonClicked)
|
||||
self.DelUserButton.clicked.connect(self.onDelUserButtonClicked)
|
||||
self.BrowseBrowserDriverButton.clicked.connect(self.onBrowseBrowserDriverButtonClicked)
|
||||
self.BrowseCurrentSystemConfigButton.clicked.connect(self.onBrowseCurrentSystemConfigButtonClicked)
|
||||
self.BrowseCurrentUserConfigButton.clicked.connect(self.onBrowseCurrentUserConfigButtonClicked)
|
||||
self.BrowseExportSystemConfigButton.clicked.connect(self.onBrowseExportSystemConfigButtonClicked)
|
||||
self.BrowseExportUserConfigButton.clicked.connect(self.onBrowseExportUserConfigButtonClicked)
|
||||
self.ExportConfigButton.clicked.connect(self.onExportConfigButtonClicked)
|
||||
self.NewConfigButton.clicked.connect(self.onNewConfigButtonClicked)
|
||||
self.LoadConfigButton.clicked.connect(self.onLoadConfigButtonClicked)
|
||||
self.ConfirmButton.clicked.connect(self.onConfirmButtonClicked)
|
||||
self.CancelButton.clicked.connect(self.onCancelButtonClicked)
|
||||
|
||||
|
||||
def closeEvent(
|
||||
self,
|
||||
event: QCloseEvent
|
||||
):
|
||||
|
||||
self.configWidgetCloseSingal.emit(self.__config_paths)
|
||||
super().closeEvent(event)
|
||||
|
||||
|
||||
def initlizeFloorRoomMap(
|
||||
self
|
||||
):
|
||||
|
||||
self.__floor_map = {
|
||||
"2": "二层",
|
||||
"3": "三层",
|
||||
"4": "四层",
|
||||
"5": "五层"
|
||||
}
|
||||
self.__room_map = {
|
||||
"1": "二层内环",
|
||||
"2": "二层外环",
|
||||
"3": "三层内环",
|
||||
"4": "三层外环",
|
||||
"5": "四层内环",
|
||||
"6": "四层外环",
|
||||
"7": "四层期刊区",
|
||||
"8": "五层考研"
|
||||
}
|
||||
self.__floor_rmap = {
|
||||
v: k for k, v in self.__floor_map.items()
|
||||
}
|
||||
self.__room_rmap = {
|
||||
v: k for k, v in self.__room_map.items()
|
||||
}
|
||||
self.__floor_room_map = {
|
||||
"二层": ["二层内环", "二层外环"],
|
||||
"三层": ["三层内环", "三层外环"],
|
||||
"四层": ["四层内环", "四层外环", "四层期刊区"],
|
||||
"五层": ["五层考研"]
|
||||
}
|
||||
|
||||
|
||||
def initlizeDefaultConfigPaths(
|
||||
self
|
||||
) -> dict:
|
||||
|
||||
script_path = sys.executable
|
||||
script_dir = QFileInfo(script_path).absoluteDir()
|
||||
return {
|
||||
"users": QDir.toNativeSeparators(script_dir.absoluteFilePath("users.json")),
|
||||
"system": QDir.toNativeSeparators(script_dir.absoluteFilePath("system.json"))
|
||||
}
|
||||
|
||||
|
||||
def initlizeDefaultConfig(
|
||||
self,
|
||||
which: str
|
||||
):
|
||||
|
||||
default_config_paths = self.initlizeDefaultConfigPaths()
|
||||
if which == "system":
|
||||
self.__system_config_data = self.defaultSystemConfig()
|
||||
self.__config_paths["system"] = default_config_paths["system"]
|
||||
self.saveSystemConfig(self.__config_paths["system"], self.__system_config_data)
|
||||
elif which == "users":
|
||||
self.__users_config_data = self.defaultUsersConfig()
|
||||
self.__config_paths["users"] = default_config_paths["users"]
|
||||
self.saveUsersConfig(self.__config_paths["users"], self.__users_config_data)
|
||||
if which == "system":
|
||||
file_type = "系统配置文件"
|
||||
elif which == "users":
|
||||
file_type = "用户配置文件"
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"提示 - AutoLibrary",
|
||||
f"{file_type}已初始化, \n"\
|
||||
f" 文件路径: {self.__config_paths[which]}"
|
||||
)
|
||||
|
||||
|
||||
def initlizeConfigToWidget(
|
||||
self,
|
||||
which: str,
|
||||
config_data: dict
|
||||
):
|
||||
|
||||
if which == "system":
|
||||
self.setSystemConfigToWidget(config_data)
|
||||
self.CurrentSystemConfigEdit.setText(self.__config_paths["system"])
|
||||
elif which == "users":
|
||||
self.initilizeUserInfoWidget()
|
||||
self.fillUsersList(config_data)
|
||||
self.CurrentUserConfigEdit.setText(self.__config_paths["users"])
|
||||
|
||||
|
||||
def defaultSystemConfig(
|
||||
self
|
||||
) -> dict:
|
||||
|
||||
return {
|
||||
"library": {
|
||||
"host_url": "http://10.1.20.7",
|
||||
"login_url": "/login"
|
||||
},
|
||||
"login": {
|
||||
"auto_captcha": True,
|
||||
"max_attempt": 3
|
||||
},
|
||||
"web_driver": {
|
||||
"driver_type": "edge",
|
||||
"driver_path": "msedgedriver.exe",
|
||||
"headless": False
|
||||
},
|
||||
"mode": {
|
||||
"run_mode": 1
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def defaultUsersConfig(
|
||||
self
|
||||
) -> dict:
|
||||
|
||||
return {
|
||||
"users": []
|
||||
}
|
||||
|
||||
|
||||
def collectSystemConfigFromWidget(
|
||||
self
|
||||
) -> dict:
|
||||
|
||||
system_config = self.defaultSystemConfig()
|
||||
# library config is never changed
|
||||
system_config["login"]["auto_captcha"] = self.AutoCaptchaCheckBox.isChecked()
|
||||
system_config["login"]["max_attempt"] = self.LoginAttemptSpinBox.value()
|
||||
system_config["web_driver"]["driver_type"] = self.BrowserTypeComboBox.currentText()
|
||||
system_config["web_driver"]["driver_path"] = self.BrowseBrowserDriverEdit.text()
|
||||
system_config["web_driver"]["headless"] = self.HeadlessCheckBox.isChecked()
|
||||
run_mode = 0
|
||||
if self.AutoReserveCheckBox.isChecked():
|
||||
run_mode |= 0x01
|
||||
if self.AutoCheckinCheckBox.isChecked():
|
||||
run_mode |= 0x02
|
||||
if self.AutoRenewalCheckBox.isChecked():
|
||||
run_mode |= 0x04
|
||||
system_config["mode"]["run_mode"] = run_mode
|
||||
return system_config
|
||||
|
||||
|
||||
def setSystemConfigToWidget(
|
||||
self,
|
||||
system_config: dict
|
||||
):
|
||||
|
||||
self.HostUrlEdit.setText(system_config["library"]["host_url"])
|
||||
self.LoginUrlEdit.setText(system_config["library"]["login_url"])
|
||||
self.AutoCaptchaCheckBox.setChecked(system_config["login"]["auto_captcha"])
|
||||
self.LoginAttemptSpinBox.setValue(system_config["login"]["max_attempt"])
|
||||
self.BrowserTypeComboBox.setCurrentText(system_config["web_driver"]["driver_type"])
|
||||
driver_path = os.path.abspath(system_config["web_driver"]["driver_path"])
|
||||
self.BrowseBrowserDriverEdit.setText(QDir.toNativeSeparators(driver_path))
|
||||
self.HeadlessCheckBox.setChecked(system_config["web_driver"]["headless"])
|
||||
run_mode = system_config["mode"]["run_mode"]
|
||||
self.AutoReserveCheckBox.setChecked(run_mode&0x01)
|
||||
self.AutoCheckinCheckBox.setChecked(run_mode&0x02)
|
||||
self.AutoRenewalCheckBox.setChecked(run_mode&0x04)
|
||||
|
||||
|
||||
def initilizeUserInfoWidget(
|
||||
self
|
||||
):
|
||||
|
||||
self.UsernameEdit.setText("")
|
||||
self.PasswordEdit.setText("")
|
||||
self.UserListWidget.setSortingEnabled(True)
|
||||
self.PasswordEdit.setEchoMode(QLineEdit.Password)
|
||||
self.ShowPasswordCheckBox.setChecked(False)
|
||||
self.FloorComboBox.setCurrentIndex(1) # use for the '__init__' to effect the signal
|
||||
self.FloorComboBox.setCurrentIndex(0)
|
||||
self.DateEdit.setDate(QDate.currentDate())
|
||||
self.DateEdit.setMinimumDate(QDate.currentDate())
|
||||
self.DateEdit.setMaximumDate(QDate.currentDate())
|
||||
if QTime.currentTime() > QTime(18, 0, 0) and QTime.currentTime() < QTime(23, 0, 0):
|
||||
self.DateEdit.setMaximumDate(QDate.currentDate().addDays(1))
|
||||
self.BeginTimeEdit.setTime(QTime.currentTime())
|
||||
self.PreferEarlyBeginTimeCheckBox.setChecked(False)
|
||||
self.MaxBeginTimeDiffSpinBox.setValue(10)
|
||||
self.EndTimeEdit.setTime(QTime.currentTime().addSecs(120*60))
|
||||
self.PreferLateEndTimeCheckBox.setChecked(False)
|
||||
self.MaxEndTimeDiffSpinBox.setValue(10)
|
||||
self.ExpectDurationSpinBox.setValue(self.BeginTimeEdit.time().secsTo(self.EndTimeEdit.time())/3600)
|
||||
self.SatisfyDurationCheckBox.setChecked(False)
|
||||
|
||||
|
||||
def collectUserConfigFromUserInfoWidget(
|
||||
self
|
||||
) -> dict:
|
||||
|
||||
user_config = {
|
||||
"username": self.UsernameEdit.text(),
|
||||
"password": self.PasswordEdit.text(),
|
||||
"reserve_info": {
|
||||
"begin_time":{},
|
||||
"end_time": {}
|
||||
}
|
||||
}
|
||||
user_config["reserve_info"]["date"] = self.DateEdit.dateTime().toString("yyyy-MM-dd")
|
||||
user_config["reserve_info"]["place"] = self.PlaceComboBox.currentText()
|
||||
user_config["reserve_info"]["floor"] = self.__floor_rmap[self.FloorComboBox.currentText()]
|
||||
user_config["reserve_info"]["room"] = self.__room_rmap[self.RoomComboBox.currentText()]
|
||||
user_config["reserve_info"]["seat_id"] = self.SeatIDEdit.text()
|
||||
user_config["reserve_info"]["begin_time"]["time"] = self.BeginTimeEdit.time().toString("HH:mm")
|
||||
user_config["reserve_info"]["begin_time"]["max_diff"] = self.MaxBeginTimeDiffSpinBox.value()
|
||||
user_config["reserve_info"]["begin_time"]["prefer_early"] = self.PreferEarlyBeginTimeCheckBox.isChecked()
|
||||
user_config["reserve_info"]["end_time"]["time"] = self.EndTimeEdit.time().toString("HH:mm")
|
||||
user_config["reserve_info"]["end_time"]["max_diff"] = self.MaxEndTimeDiffSpinBox.value()
|
||||
user_config["reserve_info"]["end_time"]["prefer_early"] = not self.PreferLateEndTimeCheckBox.isChecked()
|
||||
user_config["reserve_info"]["expect_duration"] = self.ExpectDurationSpinBox.value()
|
||||
user_config["reserve_info"]["satisfy_duration"] = self.SatisfyDurationCheckBox.isChecked()
|
||||
return user_config
|
||||
|
||||
|
||||
def collectUserConfigFromUserListWidget(
|
||||
self,
|
||||
index: int
|
||||
) -> dict:
|
||||
|
||||
user_config = self.defaultUsersConfig()
|
||||
if index < 0 or index >= self.UserListWidget.count():
|
||||
return user_config
|
||||
user_item = self.UserListWidget.item(index)
|
||||
if user_item:
|
||||
user_config = user_item.data(Qt.UserRole)
|
||||
return user_config
|
||||
|
||||
|
||||
def setUserConfigToWidget(
|
||||
self,
|
||||
user_config: dict
|
||||
) -> None:
|
||||
|
||||
try:
|
||||
self.UsernameEdit.setText(user_config["username"])
|
||||
self.PasswordEdit.setText(user_config["password"])
|
||||
self.DateEdit.setDate(QDate.fromString(user_config["reserve_info"]["date"], "yyyy-MM-dd"))
|
||||
self.PlaceComboBox.setCurrentText(user_config["reserve_info"]["place"])
|
||||
self.FloorComboBox.setCurrentText(self.__floor_map[user_config["reserve_info"]["floor"]])
|
||||
self.RoomComboBox.setCurrentText(self.__room_map[user_config["reserve_info"]["room"]])
|
||||
self.SeatIDEdit.setText(user_config["reserve_info"]["seat_id"])
|
||||
self.BeginTimeEdit.setTime(QTime.fromString(user_config["reserve_info"]["begin_time"]["time"], "H:mm"))
|
||||
self.MaxBeginTimeDiffSpinBox.setValue(user_config["reserve_info"]["begin_time"]["max_diff"])
|
||||
self.PreferEarlyBeginTimeCheckBox.setChecked(user_config["reserve_info"]["begin_time"]["prefer_early"])
|
||||
self.EndTimeEdit.setTime(QTime.fromString(user_config["reserve_info"]["end_time"]["time"], "H:mm"))
|
||||
self.MaxEndTimeDiffSpinBox.setValue(user_config["reserve_info"]["end_time"]["max_diff"])
|
||||
self.PreferLateEndTimeCheckBox.setChecked(not user_config["reserve_info"]["end_time"]["prefer_early"])
|
||||
self.ExpectDurationSpinBox.setValue(user_config["reserve_info"]["expect_duration"])
|
||||
self.SatisfyDurationCheckBox.setChecked(user_config["reserve_info"]["satisfy_duration"])
|
||||
except:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"警告 - AutoLibrary",
|
||||
"用户配置文件读取发生错误 !\n"\
|
||||
f"用户: {user_config['username']} 配置文件可能已损坏"
|
||||
)
|
||||
|
||||
|
||||
def loadSystemConfig(
|
||||
self,
|
||||
system_config_path: str
|
||||
) -> dict:
|
||||
|
||||
try:
|
||||
if not system_config_path or not os.path.exists(system_config_path):
|
||||
raise Exception("文件路径不存在")
|
||||
system_config = ConfigReader(system_config_path).getConfigs()
|
||||
if system_config and "library" in system_config\
|
||||
and "web_driver" in system_config\
|
||||
and "login" in system_config:
|
||||
return system_config
|
||||
return None
|
||||
except Exception as e:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"警告 - AutoLibrary",
|
||||
f"系统配置文件读取发生错误 ! : {e}\n"\
|
||||
f"文件路径: {system_config_path}"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def saveSystemConfig(
|
||||
self,
|
||||
system_config_path: str,
|
||||
system_config_data: dict
|
||||
) -> bool:
|
||||
|
||||
try:
|
||||
if not system_config_path:
|
||||
raise Exception("文件路径为空")
|
||||
if not system_config_data or not isinstance(system_config_data, dict):
|
||||
raise Exception("系统配置数据为空或类型错误")
|
||||
ConfigWriter(system_config_path, system_config_data)
|
||||
return True
|
||||
except Exception as e:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"警告 - AutoLibrary",
|
||||
f"配置文件写入发生错误 ! : {e}\n"\
|
||||
f"文件路径: {system_config_path}"
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def loadUsersConfig(
|
||||
self,
|
||||
users_config_path: str
|
||||
) -> dict:
|
||||
|
||||
try:
|
||||
if not users_config_path or not os.path.exists(users_config_path):
|
||||
raise Exception("文件路径不存在")
|
||||
users_config = ConfigReader(users_config_path).getConfigs()
|
||||
if users_config and "users" in users_config:
|
||||
return users_config
|
||||
return None
|
||||
except Exception as e:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"警告 - AutoLibrary",
|
||||
f"用户配置文件读取发生错误 ! : {e}\n"\
|
||||
f"文件路径: {users_config_path}"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def saveUsersConfig(
|
||||
self,
|
||||
users_config_path: str,
|
||||
users_config_data: dict
|
||||
) -> bool:
|
||||
|
||||
try:
|
||||
if not users_config_path:
|
||||
raise Exception("文件路径为空")
|
||||
if not users_config_data or not isinstance(users_config_data, dict):
|
||||
raise Exception("用户配置数据为空或类型错误")
|
||||
ConfigWriter(users_config_path, users_config_data)
|
||||
return True
|
||||
except Exception as e:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"警告 - AutoLibrary",
|
||||
f"用户配置文件写入发生错误 ! : {e}\n"\
|
||||
f"文件路径: \n{users_config_path}"
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def saveConfigs(
|
||||
self,
|
||||
system_config_path: str,
|
||||
users_config_path: str
|
||||
) -> bool:
|
||||
|
||||
if users_config_path:
|
||||
self.__users_config_data = self.defaultUsersConfig()
|
||||
for index in range(self.UserListWidget.count()):
|
||||
user_config = self.collectUserConfigFromUserListWidget(index)
|
||||
if user_config:
|
||||
self.__users_config_data["users"].append(user_config)
|
||||
if not self.saveUsersConfig(
|
||||
users_config_path,
|
||||
self.__users_config_data
|
||||
):
|
||||
return False
|
||||
if system_config_path:
|
||||
self.__system_config_data = self.collectSystemConfigFromWidget()
|
||||
if not self.saveSystemConfig(
|
||||
system_config_path,
|
||||
self.__system_config_data
|
||||
):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def loadConfig(
|
||||
self,
|
||||
config_path: str
|
||||
) -> bool:
|
||||
|
||||
if not config_path:
|
||||
config_path = QFileDialog.getOpenFileName(
|
||||
self,
|
||||
"从现有配置文件中加载 - AutoLibrary",
|
||||
f"{QDir.toNativeSeparators(QDir.currentPath())}",
|
||||
"JSON 文件 (*.json);;所有文件 (*)"
|
||||
)[0]
|
||||
if not config_path:
|
||||
return False
|
||||
try:
|
||||
system_config = self.loadSystemConfig(config_path)
|
||||
users_config = self.loadUsersConfig(config_path)
|
||||
if system_config is not None:
|
||||
self.__system_config_data.update(system_config)
|
||||
self.setSystemConfigToWidget(self.__system_config_data)
|
||||
return True
|
||||
if users_config is not None:
|
||||
self.__users_config_data.update(users_config)
|
||||
self.fillUsersList(self.__users_config_data)
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
def fillUsersList(
|
||||
self,
|
||||
users_config_data: list[dict]
|
||||
):
|
||||
|
||||
self.UserListWidget.clear()
|
||||
if "users" in users_config_data:
|
||||
for user in users_config_data["users"]:
|
||||
user_item = QListWidgetItem(user["username"])
|
||||
user_item.setData(Qt.UserRole, user)
|
||||
self.UserListWidget.addItem(user_item)
|
||||
|
||||
|
||||
def addUser(
|
||||
self
|
||||
):
|
||||
|
||||
new_user = {
|
||||
"username": f"新用户-{self.UserListWidget.count()}",
|
||||
"password": "000000",
|
||||
"reserve_info": {
|
||||
"date": f"{QDate.currentDate().toString("yyyy-MM-dd")}",
|
||||
"place": "\u56fe\u4e66\u9986",
|
||||
"floor": "2",
|
||||
"room": "1",
|
||||
"seat_id": "",
|
||||
"begin_time": {
|
||||
"time": f"{QTime.currentTime().toString("hh:mm")}",
|
||||
"max_diff": 0,
|
||||
"prefer_early": False
|
||||
},
|
||||
"end_time": {
|
||||
"time": f"{QTime.currentTime().addSecs(2*3600).toString("hh:mm")}",
|
||||
"max_diff": 0,
|
||||
"prefer_early": True
|
||||
},
|
||||
"expect_duration": 2.0,
|
||||
"satisfy_duration": False
|
||||
}
|
||||
}
|
||||
user_item = QListWidgetItem(new_user["username"])
|
||||
user_item.setData(Qt.UserRole, new_user)
|
||||
self.UserListWidget.addItem(user_item)
|
||||
self.UserListWidget.setCurrentItem(user_item)
|
||||
self.setUserConfigToWidget(new_user)
|
||||
|
||||
|
||||
def delUser(
|
||||
self
|
||||
):
|
||||
|
||||
current_item = self.UserListWidget.currentItem()
|
||||
if current_item:
|
||||
self.UserListWidget.takeItem(self.UserListWidget.row(current_item))
|
||||
self.UserListWidget.setCurrentItem(None)
|
||||
|
||||
@Slot()
|
||||
def onShowPasswordCheckBoxChecked(
|
||||
self,
|
||||
checked: bool
|
||||
):
|
||||
|
||||
if checked:
|
||||
self.PasswordEdit.setEchoMode(QLineEdit.Normal)
|
||||
else:
|
||||
self.PasswordEdit.setEchoMode(QLineEdit.Password)
|
||||
|
||||
@Slot()
|
||||
def onFloorComboBoxCurrentIndexChanged(
|
||||
self,
|
||||
):
|
||||
|
||||
floor = self.FloorComboBox.currentText()
|
||||
self.RoomComboBox.clear()
|
||||
self.RoomComboBox.addItems(self.__floor_room_map[floor])
|
||||
self.RoomComboBox.setCurrentIndex(0)
|
||||
|
||||
@Slot()
|
||||
def onUserListWidgetCurrentItemChanged(
|
||||
self,
|
||||
current: QListWidgetItem,
|
||||
previous: QListWidgetItem
|
||||
):
|
||||
# dont care about the 'self.__users_config_data', we already
|
||||
# cant effectively update the data of each user, due to the
|
||||
# possiblity of frequency edit. we just let the QListWidget
|
||||
# help us.
|
||||
if not current:
|
||||
self.initilizeUserInfoWidget()
|
||||
return
|
||||
if previous:
|
||||
user = self.collectUserConfigFromUserInfoWidget()
|
||||
if user:
|
||||
previous.setText(user["username"])
|
||||
previous.setData(Qt.UserRole, user)
|
||||
user = current.data(Qt.UserRole)
|
||||
if user:
|
||||
self.setUserConfigToWidget(user)
|
||||
|
||||
@Slot()
|
||||
def onAddUserButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
self.addUser()
|
||||
|
||||
@Slot()
|
||||
def onDelUserButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
self.delUser()
|
||||
|
||||
@Slot()
|
||||
def onBrowseBrowserDriverButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
browser_driver_path = QFileDialog.getOpenFileName(
|
||||
self,
|
||||
"选择浏览器驱动 - AutoLibrary",
|
||||
self.CurrentSystemConfigEdit.text(),
|
||||
"可执行文件 (*.exe);;所有文件 (*)"
|
||||
)[0]
|
||||
if browser_driver_path:
|
||||
self.BrowseBrowserDriverEdit.setText(QDir.toNativeSeparators(browser_driver_path))
|
||||
|
||||
@Slot()
|
||||
def onBrowseCurrentSystemConfigButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
system_config_path = QFileDialog.getOpenFileName(
|
||||
self,
|
||||
"选择其它的系统配置 - AutoLibrary",
|
||||
self.CurrentSystemConfigEdit.text(),
|
||||
"JSON 文件 (*.json);;所有文件 (*)"
|
||||
)[0]
|
||||
if system_config_path:
|
||||
system_config_path = QDir.toNativeSeparators(system_config_path)
|
||||
if self.loadConfig(system_config_path):
|
||||
self.__config_paths["system"] = system_config_path
|
||||
self.CurrentSystemConfigEdit.setText(system_config_path)
|
||||
|
||||
@Slot()
|
||||
def onBrowseCurrentUserConfigButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
users_config_path = QFileDialog.getOpenFileName(
|
||||
self,
|
||||
"选择其它的用户配置 - AutoLibrary",
|
||||
self.CurrentUserConfigEdit.text(),
|
||||
"JSON 文件 (*.json);;所有文件 (*)"
|
||||
)[0]
|
||||
if users_config_path:
|
||||
users_config_path = QDir.toNativeSeparators(users_config_path)
|
||||
if self.loadConfig(users_config_path):
|
||||
self.__config_paths["users"] = users_config_path
|
||||
self.CurrentUserConfigEdit.setText(users_config_path)
|
||||
|
||||
@Slot()
|
||||
def onBrowseExportSystemConfigButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
system_config_path = QFileDialog.getSaveFileName(
|
||||
self,
|
||||
"导出系统配置 - AutoLibrary",
|
||||
self.CurrentSystemConfigEdit.text(),
|
||||
"JSON 文件 (*.json);;所有文件 (*)"
|
||||
)[0]
|
||||
if system_config_path:
|
||||
self.ExportSystemConfigEdit.setText(QDir.toNativeSeparators(system_config_path))
|
||||
|
||||
@Slot()
|
||||
def onBrowseExportUserConfigButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
users_config_path = QFileDialog.getSaveFileName(
|
||||
self,
|
||||
"导出用户配置 - AutoLibrary",
|
||||
self.CurrentUserConfigEdit.text(),
|
||||
"JSON 文件 (*.json);;所有文件 (*)"
|
||||
)[0]
|
||||
if users_config_path:
|
||||
self.ExportUserConfigEdit.setText(QDir.toNativeSeparators(users_config_path))
|
||||
|
||||
@Slot()
|
||||
def onExportConfigButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
msg = ""
|
||||
|
||||
system_config_path = self.ExportSystemConfigEdit.text()
|
||||
users_config_path = self.ExportUserConfigEdit.text()
|
||||
if system_config_path:
|
||||
if self.saveConfigs(
|
||||
system_config_path,
|
||||
users_config_path=""
|
||||
):
|
||||
msg += f"系统配置文件已导出到: \n'{system_config_path}'\n"
|
||||
if users_config_path:
|
||||
if self.saveConfigs(
|
||||
"", users_config_path
|
||||
):
|
||||
msg += f"用户配置文件已导出到: \n'{users_config_path}'\n"
|
||||
if msg:
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"信息 - AutoLibrary",
|
||||
msg
|
||||
)
|
||||
|
||||
@Slot()
|
||||
def onLoadConfigButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
self.loadConfig("")
|
||||
|
||||
@Slot()
|
||||
def onNewConfigButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
file_path = self.CurrentSystemConfigEdit.text()
|
||||
folder_dir = QFileDialog.getExistingDirectory(
|
||||
self,
|
||||
"选择新建配置的文件夹 - AutoLibrary",
|
||||
QDir.toNativeSeparators(QFileInfo(os.path.abspath(file_path)).absoluteDir().path())
|
||||
)
|
||||
if not folder_dir:
|
||||
return
|
||||
system_config_path = QDir.toNativeSeparators(os.path.join(folder_dir, "system.json"))
|
||||
users_config_path = QDir.toNativeSeparators(os.path.join(folder_dir, "users.json"))
|
||||
system_exists = os.path.isfile(system_config_path)
|
||||
users_exists = os.path.isfile(users_config_path)
|
||||
if system_exists or users_exists:
|
||||
exist_files = []
|
||||
if system_exists:
|
||||
exist_files.append(system_config_path)
|
||||
if users_exists:
|
||||
exist_files.append(users_config_path)
|
||||
reply = QMessageBox.information(
|
||||
self,
|
||||
"信息 - AutoLibrary",
|
||||
f"文件夹中已存在以下文件, 是否覆盖 ?\n{chr(10).join(exist_files)}",
|
||||
QMessageBox.Yes | QMessageBox.No,
|
||||
QMessageBox.No
|
||||
)
|
||||
if reply == QMessageBox.No:
|
||||
return
|
||||
self.__system_config_data = self.defaultSystemConfig()
|
||||
self.__users_config_data = self.defaultUsersConfig()
|
||||
self.__config_paths = {
|
||||
"system": system_config_path,
|
||||
"users": users_config_path
|
||||
}
|
||||
self.initlizeConfigToWidget("system", self.__system_config_data)
|
||||
self.initlizeConfigToWidget("users", self.__users_config_data)
|
||||
|
||||
@Slot()
|
||||
def onConfirmButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
if self.UserListWidget.currentItem() is not None:
|
||||
user = self.collectUserConfigFromUserInfoWidget()
|
||||
if user:
|
||||
self.UserListWidget.currentItem().setData(Qt.UserRole, user)
|
||||
if self.saveConfigs(
|
||||
self.__config_paths["system"],
|
||||
self.__config_paths["users"]
|
||||
):
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"信息 - AutoLibrary",
|
||||
"配置文件保存成功 !\n"
|
||||
f"系统配置文件路径: \n{self.__config_paths['system']}\n"\
|
||||
f"用户配置文件路径: \n{self.__config_paths['users']}"
|
||||
)
|
||||
else:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"警告 - AutoLibrary",
|
||||
"配置文件保存失败, 请检查文件路径权限"
|
||||
)
|
||||
self.close()
|
||||
|
||||
@Slot()
|
||||
def onCancelButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
self.close()
|
||||
@@ -1,310 +0,0 @@
|
||||
# -*- 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 queue
|
||||
|
||||
from PySide6.QtCore import (
|
||||
Qt, Signal, Slot, QDir, QFileInfo, QTimer, QThread
|
||||
)
|
||||
from PySide6.QtWidgets import (
|
||||
QMainWindow, QMenu
|
||||
)
|
||||
from PySide6.QtGui import (
|
||||
QTextCursor, QCloseEvent, QFont, QIcon
|
||||
)
|
||||
|
||||
from .Ui_ALMainWindow import Ui_ALMainWindow
|
||||
from .ALConfigWidget import ALConfigWidget
|
||||
|
||||
from . import AutoLibraryResource
|
||||
|
||||
from AutoLib import AutoLib
|
||||
from 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
|
||||
):
|
||||
|
||||
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"]),
|
||||
)
|
||||
auto_lib.close()
|
||||
self.showTraceSignal.emit("AutoLibrary 运行结束")
|
||||
except Exception as e:
|
||||
self.showTraceSignal.emit(
|
||||
f"AutoLibrary 运行时发生异常:{e}"
|
||||
)
|
||||
finally:
|
||||
self.finishedSignal.emit()
|
||||
|
||||
|
||||
def stop(
|
||||
self
|
||||
):
|
||||
|
||||
self.__stopped = True
|
||||
|
||||
|
||||
class ALMainWindow(QMainWindow, Ui_ALMainWindow):
|
||||
|
||||
def __init__(
|
||||
self
|
||||
):
|
||||
|
||||
super().__init__()
|
||||
self.__class_name = self.__class__.__name__
|
||||
|
||||
self.setupUi(self)
|
||||
self.__input_queue = queue.Queue()
|
||||
self.__output_queue = queue.Queue()
|
||||
self.__auto_lib = AutoLib(
|
||||
self.__input_queue,
|
||||
self.__output_queue,
|
||||
)
|
||||
self.__config_paths = {
|
||||
"system":
|
||||
f"{QDir.toNativeSeparators(QFileInfo(sys.executable).absoluteDir().absoluteFilePath("system.json"))}",
|
||||
"users":
|
||||
f"{QDir.toNativeSeparators(QFileInfo(sys.executable).absoluteDir().absoluteFilePath("users.json"))}",
|
||||
}
|
||||
self.__alConfigWidget = None
|
||||
self.__auto_lib_thread = None
|
||||
|
||||
self.modifyUi()
|
||||
self.connectSignals()
|
||||
self.startMsgPolling()
|
||||
|
||||
|
||||
def modifyUi(
|
||||
self
|
||||
):
|
||||
|
||||
icon = QIcon(":/res/icon/icons/AutoLibrary.ico")
|
||||
self.setWindowIcon(icon)
|
||||
self.MessageIOTextEdit.setFont(QFont("Courier New", 10))
|
||||
|
||||
|
||||
def connectSignals(
|
||||
self
|
||||
):
|
||||
|
||||
self.ConfigButton.clicked.connect(self.onConfigButtonClicked)
|
||||
self.StartButton.clicked.connect(self.onStartButtonClicked)
|
||||
self.StopButton.clicked.connect(self.onStopButtonClicked)
|
||||
self.SendButton.clicked.connect(self.onSendButtonClicked)
|
||||
self.MessageEdit.returnPressed.connect(self.onSendButtonClicked)
|
||||
|
||||
|
||||
def closeEvent(
|
||||
self,
|
||||
event: QCloseEvent,
|
||||
):
|
||||
|
||||
if self.__timer and self.__timer.isActive():
|
||||
self.__timer.stop()
|
||||
if self.__auto_lib:
|
||||
self.__auto_lib.close()
|
||||
if self.__alConfigWidget:
|
||||
self.__alConfigWidget.close()
|
||||
super().closeEvent(event)
|
||||
|
||||
|
||||
def appendToTextEdit(
|
||||
self,
|
||||
text: str,
|
||||
):
|
||||
|
||||
cursor = self.MessageIOTextEdit.textCursor()
|
||||
cursor.movePosition(QTextCursor.End)
|
||||
cursor.insertText(text + "\n")
|
||||
self.MessageIOTextEdit.setTextCursor(cursor)
|
||||
self.MessageIOTextEdit.ensureCursorVisible()
|
||||
scrollbar = self.MessageIOTextEdit.verticalScrollBar()
|
||||
scrollbar.setValue(scrollbar.maximum())
|
||||
|
||||
|
||||
def startMsgPolling(
|
||||
self
|
||||
):
|
||||
|
||||
self.__timer = QTimer()
|
||||
self.__timer.timeout.connect(self.pollMsgQueue)
|
||||
self.__timer.start(100)
|
||||
|
||||
|
||||
def setControlButtons(
|
||||
self,
|
||||
config_button_enabled: bool,
|
||||
start_button_enabled: bool,
|
||||
stop_button_enabled: bool,
|
||||
):
|
||||
|
||||
self.ConfigButton.setEnabled(config_button_enabled)
|
||||
self.StartButton.setEnabled(start_button_enabled)
|
||||
self.StopButton.setEnabled(stop_button_enabled)
|
||||
|
||||
@Slot()
|
||||
def showMsg(
|
||||
self,
|
||||
msg: str,
|
||||
):
|
||||
|
||||
self.appendToTextEdit(f"[{self.__class_name:<12}] >>> : {msg}")
|
||||
|
||||
@Slot()
|
||||
def showTrace(
|
||||
self,
|
||||
msg: str,
|
||||
):
|
||||
|
||||
timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
|
||||
self.appendToTextEdit(f"{timestamp}-[{self.__class_name:<12}] : {msg}")
|
||||
|
||||
@Slot()
|
||||
def pollMsgQueue(
|
||||
self,
|
||||
):
|
||||
|
||||
try:
|
||||
while True:
|
||||
msg = self.__output_queue.get_nowait()
|
||||
self.appendToTextEdit(msg)
|
||||
except queue.Empty:
|
||||
pass
|
||||
|
||||
@Slot(dict)
|
||||
def onConfigWidgetClosed(
|
||||
self,
|
||||
config_paths: dict,
|
||||
):
|
||||
|
||||
self.__alConfigWidget = None
|
||||
self.ConfigButton.setEnabled(True)
|
||||
self.StartButton.setEnabled(True)
|
||||
self.StopButton.setEnabled(False)
|
||||
self.__config_paths = config_paths
|
||||
|
||||
@Slot()
|
||||
def onConfigButtonClicked(
|
||||
self,
|
||||
):
|
||||
|
||||
if self.__alConfigWidget is None:
|
||||
self.__alConfigWidget = ALConfigWidget(
|
||||
self,
|
||||
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()
|
||||
self.ConfigButton.setEnabled(False)
|
||||
|
||||
@Slot()
|
||||
def onStartButtonClicked(
|
||||
self,
|
||||
):
|
||||
|
||||
self.setControlButtons(False, False, True)
|
||||
self.__auto_lib_thread = AutoLibWorker(
|
||||
self.__input_queue,
|
||||
self.__output_queue,
|
||||
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.start()
|
||||
|
||||
@Slot()
|
||||
def onStopButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
if self.__auto_lib_thread and self.__auto_lib_thread.isRunning():
|
||||
self.__auto_lib_thread.stop()
|
||||
self.showTrace("正在停止操作......")
|
||||
self.setControlButtons(True, True, False)
|
||||
|
||||
@Slot()
|
||||
def onSendButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
msg = self.MessageEdit.text().strip()
|
||||
if not msg:
|
||||
return
|
||||
self.showMsg(msg)
|
||||
self.MessageEdit.clear()
|
||||
@@ -0,0 +1,7 @@
|
||||
Copyright 2025 KenanZhu
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -0,0 +1 @@
|
||||
This folder is used to store the model using by ddddocr.
|
||||
@@ -1 +0,0 @@
|
||||
This folder is used to store the models by ddddocr.
|
||||
@@ -0,0 +1,118 @@
|
||||
|
||||
# AutoLibrary
|
||||
---
|
||||
|
||||

|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
了解更多请访问 [_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 协议免费开源的工具_
|
||||
Binary file not shown.
@@ -9,7 +9,7 @@ See the LICENSE file for details.
|
||||
"""
|
||||
import queue
|
||||
|
||||
from MsgBase import MsgBase
|
||||
from base.MsgBase import MsgBase
|
||||
|
||||
|
||||
class LibOperator(MsgBase):
|
||||
@@ -17,14 +17,14 @@ class LibOperator(MsgBase):
|
||||
def __init__(
|
||||
self,
|
||||
input_queue: queue.Queue,
|
||||
output_queue: queue.Queue,
|
||||
output_queue: queue.Queue
|
||||
):
|
||||
|
||||
super().__init__(input_queue, output_queue)
|
||||
|
||||
|
||||
def _waitResponseLoad(
|
||||
self,
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
pass
|
||||
@@ -16,7 +16,7 @@ class MsgBase:
|
||||
def __init__(
|
||||
self,
|
||||
input_queue: queue.Queue,
|
||||
output_queue: queue.Queue,
|
||||
output_queue: queue.Queue
|
||||
):
|
||||
|
||||
self._class_name = self.__class__.__name__
|
||||
@@ -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,12 +38,12 @@ 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(
|
||||
self,
|
||||
timeout: float = 1.0,
|
||||
timeout: float = 1.0
|
||||
) -> str:
|
||||
|
||||
try:
|
||||
@@ -55,7 +55,7 @@ class MsgBase:
|
||||
|
||||
def _inputMsg(
|
||||
self,
|
||||
timeout: float = 1.0,
|
||||
timeout: float = 1.0
|
||||
) -> bool:
|
||||
|
||||
try:
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
"""
|
||||
Base module for the AutoLibrary project.
|
||||
|
||||
Here are the classes and modules in this package:
|
||||
- MsgBase: Base class for messages.\
|
||||
- LibOperator: Base class for library operators.
|
||||
"""
|
||||
@@ -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))
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
@@ -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>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,424 @@
|
||||
# -*- 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 queue
|
||||
|
||||
from PySide6.QtCore import (
|
||||
Qt, Signal, Slot, QDir, QFileInfo, QTimer, QUrl,
|
||||
)
|
||||
from PySide6.QtWidgets import (
|
||||
QMainWindow, QMenu, QSystemTrayIcon
|
||||
)
|
||||
from PySide6.QtGui import (
|
||||
QTextCursor, QCloseEvent, QFont, QIcon, QDesktopServices
|
||||
)
|
||||
|
||||
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 gui import AutoLibraryResource
|
||||
|
||||
from utils.ConfigReader import ConfigReader
|
||||
from utils.ConfigWriter import ConfigWriter
|
||||
|
||||
|
||||
class ALMainWindow(QMainWindow, Ui_ALMainWindow):
|
||||
|
||||
timerTaskIsRunning = Signal(dict)
|
||||
timerTaskIsExecuted = Signal(dict)
|
||||
timerTaskIsError = Signal(dict)
|
||||
|
||||
def __init__(
|
||||
self
|
||||
):
|
||||
|
||||
super().__init__()
|
||||
self.__class_name = self.__class__.__name__
|
||||
|
||||
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 = {
|
||||
"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
|
||||
):
|
||||
|
||||
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(
|
||||
self
|
||||
):
|
||||
|
||||
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)
|
||||
self.MessageEdit.returnPressed.connect(self.onSendButtonClicked)
|
||||
|
||||
|
||||
def closeEvent(
|
||||
self,
|
||||
event: QCloseEvent
|
||||
):
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def appendToTextEdit(
|
||||
self,
|
||||
text: str
|
||||
):
|
||||
|
||||
cursor = self.MessageIOTextEdit.textCursor()
|
||||
cursor.movePosition(QTextCursor.End)
|
||||
cursor.insertText(text + "\n")
|
||||
self.MessageIOTextEdit.setTextCursor(cursor)
|
||||
self.MessageIOTextEdit.ensureCursorVisible()
|
||||
scrollbar = self.MessageIOTextEdit.verticalScrollBar()
|
||||
scrollbar.setValue(scrollbar.maximum())
|
||||
|
||||
|
||||
def startMsgPolling(
|
||||
self
|
||||
):
|
||||
|
||||
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,
|
||||
stop_button_enabled: bool,
|
||||
start_button_enabled: bool
|
||||
):
|
||||
|
||||
# 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(
|
||||
self,
|
||||
msg: str
|
||||
):
|
||||
|
||||
self.__output_queue.put(f"[{self.__class_name:<15}] >>> : {msg}")
|
||||
|
||||
@Slot()
|
||||
def showTrace(
|
||||
self,
|
||||
msg: str
|
||||
):
|
||||
|
||||
timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
|
||||
self.__output_queue.put(f"{timestamp}-[{self.__class_name:<15}] : {msg}")
|
||||
|
||||
@Slot()
|
||||
def pollMsgQueue(
|
||||
self
|
||||
):
|
||||
|
||||
try:
|
||||
while True:
|
||||
msg = self.__output_queue.get_nowait()
|
||||
self.appendToTextEdit(msg)
|
||||
except queue.Empty:
|
||||
pass
|
||||
|
||||
@Slot()
|
||||
def onTimerTaskWidgetClosed(
|
||||
self
|
||||
):
|
||||
|
||||
self.TimerTaskWidgetButton.setEnabled(True)
|
||||
|
||||
@Slot(dict)
|
||||
def onConfigWidgetClosed(
|
||||
self,
|
||||
config_paths: dict
|
||||
):
|
||||
|
||||
if self.__alConfigWidget:
|
||||
self.__alConfigWidget.configWidgetCloseSingal.disconnect(self.onConfigWidgetClosed)
|
||||
self.__alConfigWidget.deleteLater()
|
||||
self.__alConfigWidget = None
|
||||
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
|
||||
):
|
||||
|
||||
if self.__alConfigWidget is None:
|
||||
self.__alConfigWidget = ALConfigWidget(
|
||||
self,
|
||||
self.__config_paths
|
||||
)
|
||||
self.__alConfigWidget.configWidgetCloseSingal.connect(self.onConfigWidgetClosed)
|
||||
self.__alConfigWidget.show()
|
||||
self.__alConfigWidget.raise_()
|
||||
self.__alConfigWidget.activateWindow()
|
||||
self.ConfigButton.setEnabled(False)
|
||||
|
||||
@Slot()
|
||||
def onStartButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
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.__auto_lib_thread.finishedSignal.connect(self.onStopButtonClicked)
|
||||
self.__auto_lib_thread.finishedWithErrorSignal.connect(self.onStopButtonClicked)
|
||||
self.__auto_lib_thread.start()
|
||||
|
||||
@Slot()
|
||||
def onStopButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
if self.__auto_lib_thread:
|
||||
self.showTrace("正在停止操作......")
|
||||
self.__auto_lib_thread.wait(2000)
|
||||
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(None, False, True)
|
||||
|
||||
@Slot()
|
||||
def onSendButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
msg = self.MessageEdit.text().strip()
|
||||
if not msg:
|
||||
return
|
||||
self.showMsg(msg)
|
||||
self.__input_queue.put(msg) # put message to input queue
|
||||
self.MessageEdit.clear()
|
||||
@@ -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 "Microsoft YaHei UI";</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 "Microsoft YaHei UI";</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 "Microsoft YaHei UI";</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/>
|
||||
@@ -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)
|
||||
@@ -0,0 +1,99 @@
|
||||
# -*- 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 PySide6.QtCore import (
|
||||
Qt, Signal
|
||||
)
|
||||
from PySide6.QtWidgets import (
|
||||
QFrame, QLabel
|
||||
)
|
||||
|
||||
|
||||
class ALSeatFrame(QFrame):
|
||||
|
||||
clicked = Signal(str)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
seat_number,
|
||||
parent=None
|
||||
):
|
||||
|
||||
super().__init__(parent)
|
||||
self.__seat_number = seat_number
|
||||
self.__is_selected = False
|
||||
self.setupUi()
|
||||
|
||||
def setupUi(
|
||||
self
|
||||
):
|
||||
|
||||
self.setFixedSize(60, 40)
|
||||
self.setFrameStyle(QFrame.Box | QFrame.Plain)
|
||||
self.setLineWidth(2)
|
||||
self.setStyleSheet("""
|
||||
QFrame {
|
||||
background-color: #4196EB;
|
||||
border: 2px solid #4196EB;
|
||||
border-radius: 5px;
|
||||
}
|
||||
QLabel {
|
||||
color: #F0F0F0;
|
||||
font-weight: bold;
|
||||
}
|
||||
""")
|
||||
self.label = QLabel(self.__seat_number, self)
|
||||
self.label.setAlignment(Qt.AlignCenter)
|
||||
self.label.setGeometry(0, 0, 60, 40)
|
||||
|
||||
def mousePressEvent(
|
||||
self,
|
||||
event
|
||||
):
|
||||
|
||||
if event.button() == Qt.LeftButton:
|
||||
self.toggleSelection()
|
||||
self.clicked.emit(self.__seat_number)
|
||||
|
||||
|
||||
def isSelected(
|
||||
self
|
||||
):
|
||||
|
||||
return self.__is_selected
|
||||
|
||||
|
||||
def toggleSelection(self):
|
||||
|
||||
self.__is_selected = not self.__is_selected
|
||||
if self.__is_selected:
|
||||
self.setStyleSheet("""
|
||||
QFrame {
|
||||
background-color: #4CAF50;
|
||||
border: 2px solid #388E3C;
|
||||
border-radius: 5px;
|
||||
color: white;
|
||||
}
|
||||
QLabel {
|
||||
color: #F0F0F0;
|
||||
font-weight: bold;
|
||||
}
|
||||
""")
|
||||
else:
|
||||
self.setStyleSheet("""
|
||||
QFrame {
|
||||
background-color: #4196EB;
|
||||
border: 2px solid #4196EB;
|
||||
border-radius: 5px;
|
||||
}
|
||||
QLabel {
|
||||
color: #F0F0F0;
|
||||
font-weight: bold;
|
||||
}
|
||||
""")
|
||||
@@ -0,0 +1,270 @@
|
||||
seats_maps = {
|
||||
"2": {
|
||||
"1": """
|
||||
,,,,,,,,,,,039A,039B,,040A,040B,,041A,041B,,042A,042B,,043A,043B,,044A,044B,,,,,,,,,
|
||||
,,,,,,,,,,,039C,039D,,040C,040D,,041C,041D,,042C,042D,,043C,043D,,044C,044D,,,,,,,,,
|
||||
038B,038D,,037B,037D,,036B,036D,,,,,,,,,,,,,,,,,,,,,,045C,045A,,046C,046A,,047C,047A
|
||||
038A,038C,,037A,037C,,036A,036C,,,,,,,,,,,,,,,,,,,,,,045D,045B,,046D,046B,,047D,047B
|
||||
035B,035D,,034B,034D,,033B,033D,,,,,,,,,,,,,,,,,,,,,,048C,048A,,049C,049A,,050C,050A
|
||||
035A,035C,,034A,034C,,033A,033C,,,,,,,,,,,,,,,,,,,,,,048D,048B,,049D,049B,,050D,050B
|
||||
032B,032D,,031B,031D,,030B,030D,,,,,,,,,,,,,,,,,,,,,,051C,051A,,052C,052A,,053C,053A
|
||||
032A,032C,,031A,031C,,030A,030C,,,,,,,,,,,,,,,,,,,,,,051D,051B,,052D,052B,,053D,053B
|
||||
029B,029D,,028B,028D,,027B,027D,,,,,,,,,,,,,,,,,,,,,,054C,054A,,055C,055A,,056C,056A
|
||||
029A,029C,,028A,028C,,027A,027C,,,,,,,,,,,,,,,,,,,,,,054D,054B,,055D,055B,,056D,056B
|
||||
026B,026D,,025B,025D,,024B,024D,,,,,,,,,,,,,,,,,,,,,,057C,057A,,058C,058A,,059C,059A
|
||||
026A,026C,,025A,025C,,024A,024C,,,,,,,,,,,,,,,,,,,,,,057D,057B,,058D,058B,,059D,059B
|
||||
023B,023D,,022B,022D,,021B,021D,,,,,,,,,,,,,,,,,,,,,,060C,060A,,061C,061A,,062C,062A
|
||||
023A,023C,,022A,022C,,021A,021C,,,,,,,,,,,,,,,,,,,,,,060D,060B,,061D,061B,,062D,062B
|
||||
020B,020D,,019B,019D,,018B,018D,,,,,,,,,,,,,,,,,,,,,,063C,063A,,064C,064A,,065C,065A
|
||||
020A,020C,,019A,019C,,018A,018C,,,,,,,,,,,,,,,,,,,,,,063D,063B,,064D,064B,,065D,065B
|
||||
,,,,,,,,,,,017D,017C,,014D,014C,,011D,011C,,008D,008C,,005D,005C,,002D,002C,001D,001C,,,,,,,
|
||||
,,,,,,,,,,,017B,017A,,014B,014A,,011B,011A,,008B,008A,,005B,005A,,002B,002A,001B,001A,,,,,,,
|
||||
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,,,,,,,,073D,073C,,015D,015C,,012D,012C,,,,,006D,006C,,003D,003C,,,,,,,,,
|
||||
,,,,,,,,,,,073B,073A,,015B,015A,,012B,012A,,,,,006B,006A,,003B,003A,,,,,,,,,
|
||||
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,,,,,,,,072D,072C,,016D,016C,,013D,013C,,,,,007D,007C,,004D,004C,,,,,,,,,
|
||||
,,,,,,,,,,,072B,072A,,016B,016A,,013B,013A,,,,,007B,007A,,004B,004A,,,,,,,,,
|
||||
,,,,,,,,,,,071D,071C,,070D,070C,,069D,069C,,068D,068C,,067D,067C,,066D,066C,,,,,,,,,
|
||||
,,,,,,,,,,,071B,071A,,070B,070A,,069B,069A,,068B,068A,,067B,067A,,066B,066A,,,,,,,,,
|
||||
""",
|
||||
"2": """
|
||||
023B,023D,024B,024D,,,,,,,,,,,,,,,
|
||||
023A,023C,024A,024C,,,,,,,,,,,,,,,
|
||||
022B,022D,032D,032C,,,,,,,,,,,,,,,
|
||||
022A,022C,032B,032A,,,,,,,,,,,,,,,
|
||||
021B,021D,,,,,,,,,,,,,,,,,
|
||||
021A,021C,,,,,,,,,,,,,,,,,
|
||||
020B,020D,,,,,,,,,,,,,,,,,
|
||||
020A,020C,,,,,,,,,,,,,,,,,
|
||||
019B,019D,,,,,,,,,,,,,,,,,
|
||||
019A,019C,,,,,,,,,,,,,,,,,
|
||||
018B,018D,,,,,,,,,,,,,,,,,
|
||||
018A,018C,,,,,,,,,,,,,,,,,
|
||||
017B,017D,,,,,,,,,,,,,,,,,
|
||||
017A,017C,,,,,,,,,,,,,,,,,
|
||||
016B,016D,,,,,,,,,,,,,,,,,
|
||||
016A,016C,,,,,031A,031C,,,,,,,,,,,
|
||||
015B,015D,,,,,030B,030D,,,,,,,,,,,
|
||||
015A,015C,,,,,030A,030C,,,,,,,,,,,
|
||||
014B,014D,,,,,029B,029D,,,,,,,,,,,
|
||||
014A,014C,,,,,029A,029C,,,,,,,,,,,
|
||||
013B,013D,,,,,028B,028D,,,,,,,,,,,
|
||||
013A,013C,,,,,028A,028C,,,,,,,,,,,
|
||||
012B,012D,,,,,027B,027D,,,,,,,,,,,
|
||||
012A,012C,,,,,027A,027C,,,,,,,,,,,
|
||||
011B,011D,,,,,026B,026D,,,,,,,,,,,
|
||||
011A,011C,,,,,026A,026C,,,,,,,,,,,
|
||||
010B,010D,,,,,025B,025D,,,,,,,,,,,
|
||||
010A,010C,,,,,,,,,,,,,,,,,
|
||||
009B,009D,,,,,,,,,,,,,,,,,
|
||||
009A,009C,,,,,,,,,,,,,,,,,
|
||||
008B,008D,,,,,,,,,,,,,,,,,
|
||||
008A,008C,,,,,,,,,,,,,,,,,
|
||||
007B,007D,,,,,,,,,,,,,,,,,
|
||||
007A,007C,,,,,,,,,,,,,,,,,
|
||||
006B,006D,,,,,,,,,,,,,,,,,
|
||||
006A,006C,,,,,,,,,,,,,,,,,
|
||||
005B,005D,,,,,,,,,,,,,,,,,
|
||||
005A,005C,,,,,,,,,,,,,,,,,
|
||||
004D,004C,003D,003C,002D,002C,001D,001C,,,,,,,,,,,
|
||||
004B,004A,003B,003A,002B,002A,001B,001A,,,,,,,,,,,
|
||||
|
||||
"""
|
||||
},
|
||||
"3": {
|
||||
"3": """
|
||||
,,007B,007D,,,,,,,,008C,008A,,
|
||||
,,007A,007C,,,,,,,,008D,008B,,
|
||||
,,006B,006D,,,,,,,,009C,009A,,
|
||||
,,006A,006C,,,,,,,,009D,009B,,
|
||||
,,005B,005D,,,,,,,,010C,010a,,
|
||||
,,005A,005C,,,,,,,,010D,010B,,
|
||||
,,004B,004D,,,,,,,,011C,011A,,
|
||||
,,004A,004C,,,,,,,,011D,011B,,
|
||||
,,003B,003D,,,,,,,,012C,012A,,
|
||||
,,003A,003C,,,,,,,,012D,012B,,
|
||||
,,002B,002D,,,,,,,,013C,013A,,
|
||||
,,002A,002C,,,,,,,,013D,013B,,
|
||||
,,001B,001D,,,,,,,,014C,014A,,
|
||||
,,001A,001C,,,,,,,,014D,014B,,
|
||||
""",
|
||||
"4": """
|
||||
,,037D,037C,038D,038C,039D,039C,040D,040C,041D,041C,042D,042C,043D,043C,044D,044C,045D,045C,,,046D,046C,047D,047C,048D,048C,049D,049C,050D,050C,051D,051C,052D,052C,053D,053C,054D,054C,055D,055C,056D,056C,057D,057C,,
|
||||
,,037B,037A,038B,038A,039B,039A,040B,040A,041B,041A,042B,042A,043B,043A,044B,044A,045B,045A,,,046B,046A,047B,047A,048B,048A,049B,049A,050B,050A,051B,051A,052B,052A,053B,053A,054B,054A,055B,055A,056B,056A,057B,057A,,
|
||||
036B,036D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,058C,058A,,060C,060A
|
||||
036A,036C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,058D,058B,,060D,060B
|
||||
035B,035D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,059C,059A,,061C,061A
|
||||
035A,035C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,059D,059B,,061D,061B
|
||||
034B,034D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,062C,062A
|
||||
034A,034C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,062D,062B
|
||||
033B,033D,,,,,,,,,,,,080B,080D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,063C,063A
|
||||
033A,033C,,,,,,,,,,,,080A,080C,,081A,081B,082A,082B,083A,083B,084A,084B,085A,085B,086A,086B,087A,,,,,,,,,,,,,,,,,,063D,063B
|
||||
032B,032D,,,,,,,,,,,,079B,079D,,081C,081D,082C,082D,083C,083D,084C,084D,085C,085D,086C,086D,087C,,,,,,,,,,,,,,,,,,064C,064A
|
||||
032A,032C,,,,,,,,,,,,079A,079C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,064D,064B
|
||||
031B,031D,,,,,,,,,,,,078B,078D,,,,,,,,,,,,,,088A,088C,,,,,,,,,,,,,,,,,065C,065A
|
||||
031A,031C,,,,,,,,,,,,078A,078C,,,,,,,,,,,,,,088B,088D,,,,,,,,,,,,,,,,,065D,065B
|
||||
030B,030D,,,,,,,,,,,,077B,077D,,,,,,,,,,,,,,089A,089C,,,,,,,,,,,,,,,,,066C,066A
|
||||
030A,030C,,,,,,,,,,,,077A,077C,,,,,,,,,,,,,,089B,089D,,,,,,,,,,,,,,,,,066D,066B
|
||||
029B,029D,,,,,,,,,,,,076B,076D,,,,,,,,,,,,,,090A,090C,,,,,,,,,,,,,,,,,,
|
||||
029A,029C,,,,,,,,,,,,076A,076C,,,,,,,,,,,,,,090B,090D,,,,,,,,,,,,,,,,,,
|
||||
028B,028D,,,,,,,,,,,,075B,075D,,,,,,,,,,,,,,091A,091C,,,,,,,,,,,,,,,,,,
|
||||
028A,028C,,,,,,,,,,,,075A,075C,,,,,,,,,,,,,,091B,091D,,,,,,,,,,,,,,,,,,
|
||||
027B,027D,,,,,,,,,,,,074B,074D,,,,,,,,,,,,,,092A,092C,,,,,,,,,,,,,,,,,,
|
||||
027A,027C,,,,,,,,,,,,,,,,,,,,,,,,,,,092B,092D,,,,,,,,,,,,,,,,,,
|
||||
026B,026D,,,,,,,,,,,,,,,073D,073C,072D,072C,071D,071C,070D,070C,069D,069C,068D,068C,,,,,,,,,,,,,,,,,,,,
|
||||
026A,026C,,,,,,,,,,,,,,,073B,073A,072B,072A,071B,071A,070B,070A,069B,069A,068B,068A,,,,,,,,,,,,,,,,,,,,
|
||||
025B,025D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
025A,025C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
024B,024D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
024A,024C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
023B,023D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
023A,023C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,067C,,
|
||||
022B,022D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,067B,,
|
||||
022A,022C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,067A,,
|
||||
,,021D,021C,020D,020C,019D,019C,018D,018C,017D,017C,016D,016C,015D,015C,014D,014C,013D,013C,012D,012C,011D,011C,010D,010C,009D,009C,008D,008C,007D,007C,006D,006C,005D,005C,004D,004C,003D,003C,002D,002C,001D,001C,,,,
|
||||
,,021B,021A,020B,020A,019B,019A,018B,018A,017B,017A,016B,016A,015B,015A,014B,014A,013B,013A,012B,012A,011B,011A,010B,010A,009B,009A,008B,008A,007B,007a,006B,006A,005B,005A,004B,004A,003b,003A,002B,002A,001B,001A,,,,
|
||||
|
||||
"""
|
||||
},
|
||||
"4": {
|
||||
"5": """
|
||||
,,,,,,,,042A,042B,045A,045B,048A,048B,051A,051B,054A,054B,057A,057B,060A,060B,,,,,,
|
||||
,,,,,,,,042C,042D,045C,045D,048C,048D,051C,051D,054C,054D,057C,057D,060C,060D,,,,,,
|
||||
,,,,,,,,041A,041B,044A,044B,047A,047B,050A,050B,053A,053B,056A,056B,059A,059B,,,,,,
|
||||
,,,,,,,,041C,041D,044C,044D,047C,047D,050C,050D,053C,053D,056C,056D,059C,059D,,,,,,
|
||||
,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,,,,,040A,040B,043A,043B,046A,046B,049A,049B,052A,052B,055A,055B,058A,058B,,,,,,
|
||||
,,,,,,,,040C,040D,043C,043D,046C,046D,049C,049D,052C,052D,055C,055D,058C,058D,,,,,,
|
||||
,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
,039B,039D,038B,038D,,037B,037D,,,,,,,,,,,,,,,,,,,,,
|
||||
,039A,039C,038A,038C,,037A,037C,,,,,,,,,,,,,,,,,,,,,
|
||||
,036B,036D,035B,035D,,034B,034D,,,,,,,,,,,,,,,,,,,,,
|
||||
,036A,036C,035A,035C,,034A,034C,,,,,,,,,,,,,,,,,,,,,
|
||||
,033B,033D,032B,032D,,031B,031D,,,,,,,,,,,,,,,,,,,,,
|
||||
,033A,033C,032A,032C,,031A,031C,,,,,,,,,,,,,,,,,,,,,
|
||||
,030B,030D,029B,029D,,028B,028D,,,,,,,,,,,,,,,,,,,,,
|
||||
,030A,030C,029A,029C,,028A,028C,,,,,,,,,,,,,,,,,,,,,
|
||||
,027B,027D,026B,026D,,025B,025D,,,,,,,,,,,,,,,,,,,,,
|
||||
,027A,027C,026A,026C,,025A,025C,,,,,,,,,,,,,,,,,,,,,
|
||||
,024B,024D,023B,023D,,022B,022D,,,,,,,,,,,,,,,,,,,,,
|
||||
,024A,024C,023A,023C,,022A,022C,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,,,,,019D,019C,016D,016C,013D,013C,010D,010C,007D,007C,004D,004C,001D,001C,,,,,,
|
||||
,,,,,,,,019B,019A,016B,016A,013B,013A,010B,010A,007B,007A,004B,004A,001B,001A,,,,,,
|
||||
,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,,,,,020D,020C,017D,017C,014D,014C,011D,011C,008D,008C,005D,005C,002D,002C,,,,,,
|
||||
,,,,,,,,020B,020A,017B,017A,014B,014A,011B,011A,008B,008A,005B,005A,002B,002A,,,,,,
|
||||
,,,,,,,,021D,021C,018D,018C,015D,015C,012D,012C,009D,009C,006D,006C,003D,003C,,,,,,
|
||||
,,,,,,,,021B,021A,018B,018A,015B,015A,012B,012A,009B,009A,006B,006A,003B,003A,,,,,,
|
||||
|
||||
""",
|
||||
"6": """
|
||||
,,,026C,026D,027D,027C,028D,028C,029D,029C,030D,030C,031D,031C,032D,032C,033D,033C,035D,035C,036D,036C,037D,037C,038D,038C,039D,039C,040D,040C,041D,041C,042D,042C,043D,043C,044D,044C,045D,045C,046D,046C
|
||||
,,,026A,026B,027B,027A,028B,028A,029B,029A,030B,030A,031B,031A,032B,032A,033B,033A,035B,035A,036B,036A,037B,037A,038B,038A,039B,039A,040B,040A,041B,041A,042B,042A,043B,043A,044B,044A,045B,045A,046B,046A
|
||||
025D,025C,,,,,,,,,,,,,,,,034D,034C,,,,,,,,,,,,,,,,,,,,,,,047C,047A
|
||||
025B,025A,,,,,,,,,,,,,,,,034B,034A,,,,,,,,,,,,,,,,,,,,,,,047D,047B
|
||||
024D,024C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,048C,048A
|
||||
024B,024A,,,,,,,,,,,,,,050D,050C,052D,052C,054D,054C,056D,056C,058D,058C,060D,060C,,,,,,,,,,,,,,,048D,048B
|
||||
023D,023C,,,,,,,,,,,,,,050B,050A,052B,052A,054B,054A,056B,056A,058B,058A,060B,060A,,,,,,,,,,,,,,,,
|
||||
023B,023A,,,,,,,,,,,,,,049D,049C,051D,051C,053D,053C,055D,055C,057D,057C,059D,059C,,,,,,,,,,,,,,,,
|
||||
022D,022C,,,,,,,,,,,,,,049B,049A,051B,051A,053B,053A,055B,055A,057B,057A,059B,059A,,,,,,,,,,,,,,,,
|
||||
022B,022A,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
021D,021C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
021B,021A,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
020D,020C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
020B,020A,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
019D,019C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
019B,019A,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
015D,015C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
015B,015A,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
014D,014C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
014B,014A,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
013D,013C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
013B,013A,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
012D,012C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
012B,012A,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
011D,011C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
011B,011A,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
010D,010C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
010B,010A,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
009D,009C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
009B,009A,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
008D,008C,,007D,007C,006D,006C,005D,005C,004D,004C,003D,003C,002D,002C,001D,001C,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
008B,008A,,007B,007A,006B,006A,005B,005A,004B,004A,003B,003A,002B,002A,001B,001A,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
|
||||
""",
|
||||
"7": """
|
||||
,,,,,,,,022D,022C,021D,021C,020D,020C,019D,019C,018D,018C,017D,017C,,,,,,,,,,,,
|
||||
,,,,,,,,022B,022A,021B,021A,020B,020A,019B,019A,018B,018A,017B,017A,,,,,,,,,,,,
|
||||
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
016D,016C,015D,015C,014D,014C,013D,013C,012D,012C,011D,011C,010D,010C,009D,009C,008D,008C,007D,007C,006D,006C,005D,005C,004D,004C,003D,003C,002D,002C,001D,001C
|
||||
016B,016A,015B,015A,014B,014A,013B,013A,012B,012A,011B,011A,010B,010A,009B,009A,008B,008A,007B,007A,006B,006A,005B,005A,004B,004A,003B,003A,002B,002A,001B,001A
|
||||
|
||||
"""
|
||||
},
|
||||
"5": {
|
||||
"8": """
|
||||
,,,046D,046C,047D,047C,048D,048C,049D,049C,050D,050C,051D,051C,052D,052C,053D,053C,054D,054C,055D,055C,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,046B,046A,047B,047A,048B,048A,049B,049A,050B,050A,051B,051A,052B,052A,053B,053A,054B,054A,055B,055A,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,,,,,,,,,,,,,,,,,,,,056C,056A,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
045B,045D,,,,,,,,,,,,,,,,,,,,,,056D,056B,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
045A,045C,,,,,,,,,,,,,,,,,,,,,,057C,057A,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
044B,044D,,,,,,,,,,,,,,,,,,,,,,057D,057B,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
044A,044C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
043B,043D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
043A,043C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
042B,042D,,,,,,,,,,,,,,,,,070B,070D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
042A,042C,,,,,,,,,,,,,,,,,070A,070C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
041B,041D,,,,,,,,,,,,,,,,,069B,069D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
041A,041C,,,,,,,,,,,,,,,,,069A,069C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
040B,040D,,,,,,,,,,,,,,,,,068B,068D,,071A,071B,072A,072B,073A,073B,074A,074B,075A,075B,076A,076B,077A,077B,,,,,,,,,,,,,,,,
|
||||
040A,040C,,,,,,,,,,,,,,,,,068A,068C,,071C,071D,072C,072D,073C,073D,074C,074D,075C,075D,076C,076D,077C,077D,,,,,,,,,,,,,,,,
|
||||
039B,039D,,,,,,,,,,,,,,,,,067B,067D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
039A,039C,,,,,,,,,,,,,,,,,067A,067C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
038B,038D,,,,,,,,,,,,,,,,,066B,066D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
038A,038C,,,,,,,,,,,,,,,,,066A,066C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
037B,037D,,,,,,,,,,,,,,,,,065B,065D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
037A,037C,,,,,,,,,,,,,,,,,065A,065C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
036B,036D,,,,,,,,,,,,,,,,,064B,064D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
036A,036C,,,,,,,,,,,,,,,,,064A,064C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
035B,035D,,,,,,,,,,,,,,,,,063B,063D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
035A,035C,,,,,,,,,,,,,,,,,063A,063C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
034B,034D,,,,,,,,,,,,,,,,,062B,062D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
034A,034C,,,,,,,,,,,,,,,,,062A,062C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
033B,033D,,,,,,,,,,,,,,,,,,,061D,061C,,060D,060C,,059D,059C,,058D,058C,,,,,,,,,,,,,,,,,,,,
|
||||
033A,033C,,,,,,,,,,,,,,,,,,,061B,061A,,060B,060A,,059B,059A,,058B,058A,,,,,,,,,,,,,,,,,,,,
|
||||
032B,032D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
032A,032C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
031B,031D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
031A,031C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
030B,030D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
030A,030C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
029B,029D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
029A,029C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
028B,028D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
028A,028C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
027B,027D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
027A,027C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
026B,026D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
026A,026C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
025B,025D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
025A,025C,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,024D,024C,023D,023C,022D,022C,021D,021C,020D,020C,019D,019C,018D,018C,017D,017C,016D,016C,015D,015C,014D,014C,013D,013C,012D,012C,011D,011C,010D,010C,009D,009C,008D,008C,007D,007C,006D,006C,005D,005C,004D,004C,003D,003C,002D,002C,001D,001C
|
||||
,,,024B,024A,023B,023A,022B,022A,021B,021A,020B,020A,019B,019A,018B,018A,017B,017A,016B,016A,015B,015A,014B,014A,013B,013A,012B,012A,011B,011A,010B,010A,009B,009A,008B,008A,007B,007A,006B,006A,005B,005A,004B,004A,003B,003A,002B,002A,001B,001A
|
||||
|
||||
"""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
# -*- 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 PySide6.QtCore import (
|
||||
Qt, Slot, Signal, QEvent
|
||||
)
|
||||
from PySide6.QtWidgets import (
|
||||
QFrame, QWidget, QLabel, QHBoxLayout, QVBoxLayout,
|
||||
QGridLayout, QGraphicsView, QGraphicsScene, QGraphicsItem,
|
||||
QPushButton,
|
||||
)
|
||||
from PySide6.QtGui import (
|
||||
QPainter, QWheelEvent, QCloseEvent
|
||||
)
|
||||
from gui.ALSeatFrame import ALSeatFrame
|
||||
|
||||
|
||||
class ALSeatMapWidget(QWidget):
|
||||
|
||||
seatMapWidgetClosed = Signal(list)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent: QWidget = None,
|
||||
floor: str = "",
|
||||
room: str = "",
|
||||
seats_data: dict = {},
|
||||
):
|
||||
|
||||
super().__init__(parent)
|
||||
|
||||
self.__floor = floor
|
||||
self.__room = room
|
||||
self.__seats_data = seats_data
|
||||
self.__selected_seats = []
|
||||
self.__seat_frames = {}
|
||||
self.setupUi()
|
||||
self.connectSignals()
|
||||
|
||||
@staticmethod
|
||||
def formatSeatNumber(
|
||||
seat_number: str
|
||||
) -> str:
|
||||
|
||||
if seat_number and not seat_number[-1].isdigit():
|
||||
digits = seat_number[:-1]
|
||||
letter = seat_number[-1]
|
||||
return digits.zfill(3) + letter
|
||||
return seat_number.zfill(3)
|
||||
|
||||
|
||||
def setupUi(
|
||||
self
|
||||
):
|
||||
|
||||
self.setWindowFlags(Qt.WindowType.Window)
|
||||
self.setWindowModality(Qt.WindowModality.ApplicationModal)
|
||||
self.setMinimumSize(800, 600)
|
||||
self.resize(800, 600)
|
||||
self.setWindowTitle(f"选择楼层座位 - AutoLibrary")
|
||||
|
||||
self.SeatMapWidgetMainLayout = QVBoxLayout(self)
|
||||
self.TitleLabel = QLabel(f"楼层座位分布图: {self.__floor}-{self.__room}")
|
||||
self.TitleLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.TitleLabel.setStyleSheet("font-size: 16px; font-weight: bold; margin: 10px;")
|
||||
self.SeatMapWidgetMainLayout.addWidget(self.TitleLabel)
|
||||
|
||||
self.SeatMapGraphicsView = QGraphicsView(self)
|
||||
self.SeatMapGraphicsScene = QGraphicsScene(self)
|
||||
self.SeatMapGraphicsView.setScene(self.SeatMapGraphicsScene)
|
||||
self.SeatMapGraphicsView.setRenderHint(QPainter.RenderHint.LosslessImageRendering)
|
||||
self.SeatMapGraphicsView.setDragMode(QGraphicsView.DragMode.ScrollHandDrag)
|
||||
self.SeatMapGraphicsView.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
||||
self.SeatMapGraphicsView.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
||||
self.SeatMapGraphicsView.viewport().installEventFilter(self)
|
||||
|
||||
self.SeatsContainerWidget = QWidget()
|
||||
self.SeatsContainerLayout = QGridLayout(self.SeatsContainerWidget)
|
||||
self.createSeatMap()
|
||||
|
||||
self.ContainerProxy = self.SeatMapGraphicsScene.addWidget(self.SeatsContainerWidget)
|
||||
self.ContainerProxy.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable, False)
|
||||
self.SeatMapWidgetMainLayout.addWidget(self.SeatMapGraphicsView)
|
||||
|
||||
self.TipsLabel = QLabel(
|
||||
" 点击座位进行选择/取消选择, 最多选择1个座位 \n"
|
||||
" [操作方法: Ctrl+鼠标滚轮缩放 | 滚轮/拖拽/方向键 移动]"
|
||||
)
|
||||
self.TipsLabel.setAlignment(Qt.AlignmentFlag.AlignLeft)
|
||||
self.TipsLabel.setStyleSheet("color: #666; margin: 5px;")
|
||||
self.SeatMapWidgetMainLayout.addWidget(self.TipsLabel)
|
||||
|
||||
self.ConfirmButton = QPushButton("确认")
|
||||
self.ConfirmButton.setFixedSize(80, 25)
|
||||
self.CancelButton = QPushButton("取消")
|
||||
self.CancelButton.setFixedSize(80, 25)
|
||||
self.SeatMapWidgetControlLayout = QHBoxLayout()
|
||||
self.SeatMapWidgetControlLayout.setAlignment(Qt.AlignmentFlag.AlignRight)
|
||||
self.SeatMapWidgetControlLayout.addWidget(self.CancelButton)
|
||||
self.SeatMapWidgetControlLayout.addWidget(self.ConfirmButton)
|
||||
self.SeatMapWidgetMainLayout.addLayout(self.SeatMapWidgetControlLayout)
|
||||
|
||||
|
||||
def connectSignals(
|
||||
self
|
||||
):
|
||||
|
||||
self.ConfirmButton.clicked.connect(self.onConfirmButtonClicked)
|
||||
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
|
||||
):
|
||||
|
||||
self.seatMapWidgetClosed.emit(self.__selected_seats)
|
||||
super().closeEvent(event)
|
||||
|
||||
|
||||
def eventFilter(
|
||||
self,
|
||||
watched,
|
||||
event
|
||||
):
|
||||
|
||||
if (watched is self.SeatMapGraphicsView.viewport() and
|
||||
event.type() == QEvent.Type.Wheel and
|
||||
event.modifiers() == Qt.KeyboardModifier.ControlModifier
|
||||
):
|
||||
self.zoomGraphicsView(event)
|
||||
return True
|
||||
return super().eventFilter(watched, event)
|
||||
|
||||
|
||||
def zoomGraphicsView(
|
||||
self,
|
||||
event: QWheelEvent
|
||||
):
|
||||
|
||||
delta = event.angleDelta().y()
|
||||
zoom_factor = 1.2 if delta > 0 else 1/1.2
|
||||
self.SeatMapGraphicsView.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse)
|
||||
self.SeatMapGraphicsView.scale(zoom_factor, zoom_factor)
|
||||
|
||||
|
||||
def createSeatMap(
|
||||
self
|
||||
):
|
||||
|
||||
rows = self.__seats_data.strip().split("\n")
|
||||
for row_idx, row in enumerate(rows):
|
||||
col_idx = 0
|
||||
seats_number = [seat.strip() for seat in row.split(",")]
|
||||
for seat_number in seats_number:
|
||||
if 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
|
||||
else:
|
||||
spacer = QFrame()
|
||||
spacer.setFixedSize(20, 30)
|
||||
spacer.setStyleSheet("background-color: transparent; border: none;")
|
||||
self.SeatsContainerLayout.addWidget(spacer, row_idx, col_idx)
|
||||
col_idx += 1
|
||||
self.SeatsContainerLayout.setSpacing(20)
|
||||
self.SeatsContainerLayout.setContentsMargins(20, 20, 20, 20)
|
||||
self.SeatsContainerWidget.adjustSize()
|
||||
|
||||
|
||||
def selectSeat(
|
||||
self,
|
||||
seat_number: str
|
||||
):
|
||||
|
||||
if len(self.__selected_seats) >= 1:
|
||||
return
|
||||
seat_number = self.formatSeatNumber(seat_number)
|
||||
if seat_number not in self.__seat_frames:
|
||||
return
|
||||
widget = self.__seat_frames[seat_number]
|
||||
if widget.isSelected():
|
||||
return
|
||||
widget.toggleSelection()
|
||||
self.__selected_seats.append(seat_number)
|
||||
|
||||
|
||||
def selectSeats(
|
||||
self,
|
||||
selected_seats: list
|
||||
):
|
||||
|
||||
self.clearSelections()
|
||||
for seat_number in selected_seats:
|
||||
self.selectSeat(seat_number)
|
||||
|
||||
|
||||
def getSelectedSeats(
|
||||
self
|
||||
) -> list[str]:
|
||||
|
||||
return self.__selected_seats
|
||||
|
||||
|
||||
def clearSelections(
|
||||
self
|
||||
):
|
||||
|
||||
seats_to_clear = self.__selected_seats.copy()
|
||||
for seat_number in seats_to_clear:
|
||||
if seat_number not in self.__seat_frames:
|
||||
continue
|
||||
widget = self.__seat_frames[seat_number]
|
||||
if widget.isSelected():
|
||||
widget.toggleSelection()
|
||||
self.__selected_seats = []
|
||||
|
||||
@Slot(str)
|
||||
def onSeatClicked(
|
||||
self,
|
||||
seat_number: str
|
||||
):
|
||||
|
||||
if seat_number in self.__selected_seats:
|
||||
self.__selected_seats.remove(seat_number)
|
||||
else:
|
||||
if len(self.__selected_seats) < 1:
|
||||
self.__selected_seats.append(seat_number)
|
||||
else:
|
||||
self.__seat_frames[seat_number].toggleSelection()
|
||||
|
||||
@Slot()
|
||||
def onConfirmButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
self.close()
|
||||
|
||||
@Slot()
|
||||
def onCancelButtonClicked(
|
||||
self
|
||||
):
|
||||
|
||||
self.clearSelections()
|
||||
self.close()
|
||||
@@ -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()
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
@@ -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,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>
|
||||
@@ -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] 所有操作完成。
|
||||
@@ -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] 所有操作完成。"
|
||||
@@ -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] 所有操作完成。
|
||||
@@ -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] 所有操作完成。"
|
||||
@@ -0,0 +1 @@
|
||||
this folder is used to store the batch scripts.
|
||||
@@ -0,0 +1 @@
|
||||
this folder is used to store the config files.
|
||||
|
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 |
@@ -0,0 +1,328 @@
|
||||
# -*- 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 queue
|
||||
|
||||
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 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
|
||||
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
|
||||
|
||||
|
||||
class AutoLib(MsgBase):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
input_queue: queue.Queue,
|
||||
output_queue: queue.Queue,
|
||||
run_config: dict
|
||||
):
|
||||
super().__init__(input_queue, output_queue)
|
||||
|
||||
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(
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
self._showTrace("正在初始化浏览器驱动......")
|
||||
|
||||
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
|
||||
driver_options.add_argument("--window-size=1920,1080")
|
||||
|
||||
# omit ssl errors and verbose log level
|
||||
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")
|
||||
|
||||
# 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 = 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
|
||||
match self.__driver_type.lower():
|
||||
case "edge":
|
||||
service = EdgeService(executable_path=self.__driver_path)
|
||||
self.__driver = webdriver.Edge(service=service, options=driver_options)
|
||||
case "chrome":
|
||||
service = ChromeService(executable_path=self.__driver_path)
|
||||
self.__driver = webdriver.Chrome(service=service, options=driver_options)
|
||||
case "firefox":
|
||||
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)
|
||||
self.__driver.execute_script(
|
||||
"Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
|
||||
)
|
||||
except Exception as e:
|
||||
self._showTrace(f"浏览器驱动初始化失败: {e}")
|
||||
return False
|
||||
self._showTrace(f"浏览器驱动已初始化, 类型: {self.__driver_type}, 路径: {self.__driver_path}")
|
||||
return True
|
||||
|
||||
|
||||
def __initLibOperators(
|
||||
self
|
||||
):
|
||||
|
||||
if not self.__driver:
|
||||
self._showTrace(f"浏览器驱动未初始化, 请先初始化浏览器驱动 !")
|
||||
return
|
||||
self.__lib_checker = LibChecker(self._input_queue, self._output_queue, self.__driver)
|
||||
self.__lib_login = LibLogin(self._input_queue, self._output_queue, self.__driver)
|
||||
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(
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
# wait for page load
|
||||
try:
|
||||
WebDriverWait(self.__driver, 2).until( # title contains "首页"
|
||||
EC.title_contains("首页")
|
||||
)
|
||||
WebDriverWait(self.__driver, 2).until( # username field presence
|
||||
EC.presence_of_element_located((By.NAME, "username"))
|
||||
)
|
||||
WebDriverWait(self.__driver, 2).until( # password field presence
|
||||
EC.presence_of_element_located((By.NAME, "password"))
|
||||
)
|
||||
WebDriverWait(self.__driver, 2).until( # captcha field presence
|
||||
EC.presence_of_element_located((By.NAME, "answer"))
|
||||
)
|
||||
WebDriverWait(self.__driver, 2).until( # captcha image presence
|
||||
EC.presence_of_element_located((By.ID, "loadImgId"))
|
||||
)
|
||||
return True
|
||||
except:
|
||||
self._showTrace(f"登录页面加载失败 !")
|
||||
return False
|
||||
|
||||
|
||||
def __initDriverUrl(
|
||||
self,
|
||||
) -> bool:
|
||||
|
||||
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
|
||||
return True
|
||||
|
||||
|
||||
def __run(
|
||||
self,
|
||||
username: str,
|
||||
password: str,
|
||||
login_config: dict,
|
||||
run_mode_config: dict,
|
||||
reserve_info: dict
|
||||
) -> int:
|
||||
|
||||
# result : -1 - terminate, 0 - success, 1 - failed, 2 - passed
|
||||
result = 2
|
||||
|
||||
# login
|
||||
if not self.__lib_login.login(
|
||||
username,
|
||||
password,
|
||||
login_config.get("max_attempt", 3),
|
||||
login_config.get("auto_captcha", True),
|
||||
):
|
||||
return 1
|
||||
"""
|
||||
Here, we collect the run mode from the run config.
|
||||
"""
|
||||
run_mode = run_mode_config.get("run_mode", 0)
|
||||
run_mode = {
|
||||
"auto_reserve": run_mode&0x1,
|
||||
"auto_checkin": run_mode&0x2,
|
||||
"auto_renewal": run_mode&0x4,
|
||||
}
|
||||
# reserve
|
||||
if run_mode["auto_reserve"]:
|
||||
if self.__lib_checker.canReserve(reserve_info.get("date")):
|
||||
if self.__lib_reserve.reserve(username, reserve_info):
|
||||
result = 0
|
||||
else:
|
||||
result = 1
|
||||
else:
|
||||
self._showTrace(f"用户 {username} 无法预约,已跳过")
|
||||
result = 2
|
||||
# checkin
|
||||
if run_mode["auto_checkin"] and result == 2:
|
||||
if self.__lib_checker.canCheckin():
|
||||
if self.__lib_checkin.checkin(username):
|
||||
result = 0
|
||||
else:
|
||||
result = 1
|
||||
else:
|
||||
self._showTrace(f"用户 {username} 无法签到,已跳过")
|
||||
result = 2
|
||||
# renewal
|
||||
if run_mode["auto_renewal"] and result == 2:
|
||||
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
|
||||
):
|
||||
# if logout is failed, we must make sure the host to be reloaded
|
||||
# otherwise, the next login may fail
|
||||
if not self.__initDriverUrl():
|
||||
return -1
|
||||
return result
|
||||
|
||||
|
||||
def run(
|
||||
self,
|
||||
user_config: dict
|
||||
):
|
||||
|
||||
self.__user_config = user_config
|
||||
|
||||
user_counter = {"current": 0, "success": 0, "failed": 0, "passed": 0}
|
||||
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"]}......"
|
||||
)
|
||||
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 == -1:
|
||||
self._showTrace(
|
||||
f"用户 {user["username"]} 处理过程中页面发生异常,无法继续操作, 任务已终止 !"
|
||||
)
|
||||
break
|
||||
elif r == 0:
|
||||
user_counter["success"] += 1
|
||||
elif r == 1:
|
||||
user_counter["failed"] += 1
|
||||
elif r == 2:
|
||||
user_counter["passed"] += 1
|
||||
self._showTrace(f"处理完成, 共计 {user_counter["current"]} 个用户, "\
|
||||
f"成功 {user_counter["success"]} 个用户, "\
|
||||
f"失败 {user_counter["failed"]} 个用户, "\
|
||||
f"跳过 {user_counter["passed"]} 个用户"
|
||||
)
|
||||
return
|
||||
|
||||
|
||||
def close(
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
if self.__driver:
|
||||
if self.__driver_type.lower() == "firefox":
|
||||
self._showTrace(f"Firefox 浏览器驱动关闭略慢, 请耐心等待...")
|
||||
self.__driver.quit()
|
||||
self.__driver = None
|
||||
self._showTrace(f"浏览器驱动已关闭")
|
||||
return True
|
||||
else:
|
||||
self._showTrace(f"浏览器驱动未初始化, 无需关闭")
|
||||
return False
|
||||
@@ -0,0 +1,374 @@
|
||||
# -*- 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 re
|
||||
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
|
||||
|
||||
|
||||
class LibChecker(LibOperator):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
input_queue: queue.Queue,
|
||||
output_queue: queue.Queue,
|
||||
driver: any
|
||||
):
|
||||
|
||||
super().__init__(input_queue, output_queue)
|
||||
|
||||
self.__driver = driver
|
||||
|
||||
|
||||
def _waitResponseLoad(
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def __formatDiffTime(
|
||||
seconds: float
|
||||
) -> str:
|
||||
|
||||
hours = int(seconds//3600)
|
||||
minutes = int(seconds%3600//60)
|
||||
seconds = int(seconds%60)
|
||||
return f"{hours} 时 {minutes} 分 {seconds} 秒"
|
||||
|
||||
|
||||
def __navigateToReserveRecordPage(
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
try:
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.element_to_be_clickable((By.XPATH, "//a[@href='/history?type=SEAT']"))
|
||||
).click()
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.presence_of_element_located((By.CLASS_NAME, "myReserveList"))
|
||||
)
|
||||
except:
|
||||
self._showTrace("加载预约记录页面失败 !")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def __decodeReserveTime(
|
||||
self,
|
||||
time_element
|
||||
) -> dict:
|
||||
|
||||
time_str = time_element.text.strip()
|
||||
today = datetime.now().date()
|
||||
if "明天" in time_str:
|
||||
target_date = today + timedelta(days=1)
|
||||
date = target_date.strftime("%Y-%m-%d")
|
||||
elif "今天" in time_str:
|
||||
target_date = today
|
||||
date = target_date.strftime("%Y-%m-%d")
|
||||
elif "昨天" in time_str:
|
||||
target_date = today - timedelta(days=1)
|
||||
date = target_date.strftime("%Y-%m-%d")
|
||||
else:
|
||||
date_match = re.search(r"(\d{4}-\d{1,2}-\d{1,2})", time_str)
|
||||
if date_match:
|
||||
date = date_match.group(1)
|
||||
else:
|
||||
date = ""
|
||||
time_match = re.search(r"(\d{1,2}:\d{2}) -- (\d{1,2}:\d{2})", time_str)
|
||||
if time_match:
|
||||
begin_time = time_match.group(1)
|
||||
end_time = time_match.group(2)
|
||||
else:
|
||||
begin_time = ""
|
||||
end_time = ""
|
||||
return {
|
||||
"date": date,
|
||||
"time": {
|
||||
"begin": begin_time,
|
||||
"end": end_time
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def __decodeReserveInfo(
|
||||
self,
|
||||
info_elements
|
||||
) -> str:
|
||||
|
||||
location = ""
|
||||
status = ""
|
||||
for info in info_elements:
|
||||
if "已预约" in info.text:
|
||||
status = "已预约"
|
||||
elif "使用中" in info.text:
|
||||
status = "使用中"
|
||||
elif "已完成" in info.text:
|
||||
status = "已完成"
|
||||
elif "已结束使用" in info.text:
|
||||
status = "已结束使用"
|
||||
elif "已取消" in info.text:
|
||||
status = "已取消"
|
||||
elif "失约" in info.text:
|
||||
status = "失约"
|
||||
elif "图书馆" in info.text:
|
||||
location = info.text.strip()
|
||||
return {
|
||||
"location": location,
|
||||
"status": status,
|
||||
}
|
||||
|
||||
|
||||
def __decodeReserveRecord(
|
||||
self,
|
||||
reservation
|
||||
) -> dict:
|
||||
|
||||
try:
|
||||
time_element = reservation.find_element(
|
||||
By.CSS_SELECTOR, "dt"
|
||||
)
|
||||
info_elements = reservation.find_elements(
|
||||
By.CSS_SELECTOR, "a"
|
||||
)
|
||||
except:
|
||||
return {
|
||||
"date": "",
|
||||
"time": {"begin": "", "end": ""},
|
||||
"info": {"location": "", "status": ""}
|
||||
}
|
||||
time = self.__decodeReserveTime(time_element)
|
||||
info = self.__decodeReserveInfo(info_elements)
|
||||
return {
|
||||
"date": time["date"],
|
||||
"time": time["time"],
|
||||
"info": info
|
||||
}
|
||||
|
||||
|
||||
def __loadReserveRecords(
|
||||
self
|
||||
) -> list:
|
||||
try:
|
||||
# check if there's any reservation on the date
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.presence_of_element_located((By.CSS_SELECTOR, ".myReserveList > dl"))
|
||||
)
|
||||
reservations = self.__driver.find_elements(
|
||||
By.CSS_SELECTOR, ".myReserveList > dl:not(#moreBlock)"
|
||||
)
|
||||
return reservations
|
||||
except:
|
||||
self._showTrace("加载预约记录失败 !")
|
||||
return None
|
||||
|
||||
|
||||
def __showMoreReserveRecords(
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
# load new reservations if still not sure
|
||||
try:
|
||||
WebDriverWait(self.__driver, 0.1).until(
|
||||
EC.element_to_be_clickable((By.ID, "moreBtn"))
|
||||
)
|
||||
except:
|
||||
# the reservation is the last one
|
||||
return False
|
||||
try:
|
||||
more_btn = self.__driver.find_element(By.ID, "moreBtn")
|
||||
if more_btn.is_displayed() and more_btn.is_enabled():
|
||||
self.__driver.execute_script("arguments[0].scrollIntoView(true);", more_btn)
|
||||
self.__driver.execute_script("arguments[0].click();", more_btn)
|
||||
return True
|
||||
else:
|
||||
self._showTrace("用户无法加载更多预约记录")
|
||||
return False
|
||||
except:
|
||||
self._showTrace("加载更多预约记录失败 !")
|
||||
return False
|
||||
|
||||
|
||||
def __getReserveRecord(
|
||||
self,
|
||||
wanted_date: str,
|
||||
wanted_status: str
|
||||
) -> dict:
|
||||
|
||||
if wanted_date is None:
|
||||
self._showTrace("日期未指定, 无法检查当前预约状态")
|
||||
return None
|
||||
self._showTrace(f"正在检查用户在 {wanted_date} 是否有预约状态为 {wanted_status} 的预约记录......")
|
||||
|
||||
checked_count = 0
|
||||
max_check_times = 6 # we only check (4*(6-1)=)20 reservations, the last time cant be checked
|
||||
|
||||
if not self.__navigateToReserveRecordPage():
|
||||
return None
|
||||
for _ in range(max_check_times):
|
||||
reservations = self.__loadReserveRecords()
|
||||
if reservations is None:
|
||||
return None
|
||||
for reservation in reservations[checked_count:]:
|
||||
record = self.__decodeReserveRecord(reservation)
|
||||
checked_count += 1
|
||||
if record is None:
|
||||
continue
|
||||
if record["date"] == "":
|
||||
continue
|
||||
if record["time"] == {"begin": "", "end": ""}:
|
||||
continue
|
||||
# record date is later than the given date, check the next one
|
||||
if datetime.strptime(record["date"], "%Y-%m-%d").date() >\
|
||||
datetime.strptime(wanted_date, "%Y-%m-%d").date():
|
||||
continue
|
||||
# record date is earlier than the given date, so there is no wanted record
|
||||
if datetime.strptime(record["date"], "%Y-%m-%d").date() <\
|
||||
datetime.strptime(wanted_date, "%Y-%m-%d").date():
|
||||
return None
|
||||
if record["info"]["status"] == wanted_status:
|
||||
self._showTrace(
|
||||
f"寻找到用户第 {checked_count} 条状态为 {wanted_status} 的预约记录, "
|
||||
f"详细信息: {record["date"]} "
|
||||
f"{record["time"]["begin"]} - {record["time"]["end"]} {record["info"]["location"]}"
|
||||
)
|
||||
return record
|
||||
if not self.__showMoreReserveRecords():
|
||||
break
|
||||
return None
|
||||
|
||||
|
||||
def canReserve(
|
||||
self,
|
||||
date: str
|
||||
) -> bool:
|
||||
|
||||
# no reserved or using record in the given date
|
||||
# then can reserve
|
||||
if self.__getReserveRecord(date, "已预约") is None:
|
||||
if self.__getReserveRecord(date, "使用中") is None:
|
||||
self._showTrace(f"用户在 {date} 可以预约")
|
||||
return True
|
||||
self._showTrace(f"用户在 {date} 有使用中的预约, 无法预约")
|
||||
return False
|
||||
self._showTrace(f"用户在 {date} 已存在有效预约, 无法预约")
|
||||
return False
|
||||
|
||||
|
||||
def canCheckin(
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
# 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"]
|
||||
begin_time = datetime.strptime(f"{date} {begin_time}", "%Y-%m-%d %H:%M")
|
||||
time_diff = datetime.now() - begin_time
|
||||
time_diff_seconds = time_diff.total_seconds()
|
||||
# before 30 minutes, cant checkin
|
||||
if time_diff_seconds < -30*60:
|
||||
self._showTrace(
|
||||
f"用户在 {date} 的预约开始时间为 {begin_time}, "
|
||||
f"当前距离预约开始时间还有 {self.__formatDiffTime(abs(time_diff_seconds))}, 无法签到"
|
||||
)
|
||||
return False
|
||||
# before in 30 minutes, can checkin
|
||||
elif -30*60 <= time_diff_seconds < 0:
|
||||
self._showTrace(
|
||||
f"用户在 {date} 的预约开始时间为 {begin_time}, "
|
||||
f"当前距离预约开始时间还有 {self.__formatDiffTime(abs(time_diff_seconds))}, 可以签到"
|
||||
)
|
||||
return True
|
||||
# past less than 30 minutes, can checkin
|
||||
elif 0 <= time_diff_seconds < 30*60 - 5: # spare 5 seconds for the checkin process
|
||||
self._showTrace(
|
||||
f"用户在 {date} 的预约开始时间为 {begin_time}, "
|
||||
f"当前距离预约开始时间已经过去 {self.__formatDiffTime(abs(time_diff_seconds))}, 可以签到"
|
||||
)
|
||||
return True
|
||||
self._showTrace(f"用户在 {date} 没有有效预约记录, 无法签到")
|
||||
return False
|
||||
|
||||
|
||||
def canRenew(
|
||||
self
|
||||
):
|
||||
|
||||
# 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"]
|
||||
end_time = datetime.strptime(f"{date} {end_time}", "%Y-%m-%d %H:%M")
|
||||
time_diff = end_time - datetime.now()
|
||||
time_diff_seconds = time_diff.total_seconds()
|
||||
# a using record is definitely after the begin time
|
||||
trace_msg = (
|
||||
f"用户在 {date} 的预约结束时间为 {end_time}, "
|
||||
f"当前距离预约结束时间还有 {self.__formatDiffTime(abs(time_diff_seconds))}"
|
||||
)
|
||||
if abs(time_diff_seconds) < 120*60:
|
||||
self._showTrace(f"{trace_msg}, 可以续约")
|
||||
return record
|
||||
else:
|
||||
self._showTrace(f"{trace_msg}, 无法续约")
|
||||
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
|
||||
@@ -0,0 +1,116 @@
|
||||
# -*- 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 re
|
||||
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
|
||||
|
||||
|
||||
class LibCheckin(LibOperator):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
input_queue: queue.Queue,
|
||||
output_queue: queue.Queue,
|
||||
driver: any
|
||||
):
|
||||
|
||||
super().__init__(input_queue, output_queue)
|
||||
|
||||
self.__driver = driver
|
||||
|
||||
|
||||
def _waitResponseLoad(
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
try:
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.presence_of_element_located((By.CLASS_NAME, "ui_dialog"))
|
||||
)
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.presence_of_element_located((By.CLASS_NAME, "resultMessage"))
|
||||
)
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.element_to_be_clickable((By.CLASS_NAME, "btnOK"))
|
||||
)
|
||||
result_message_element = self.__driver.find_element(
|
||||
By.CLASS_NAME, "resultMessage"
|
||||
)
|
||||
ok_btn = self.__driver.find_element(By.CLASS_NAME, "btnOK")
|
||||
except:
|
||||
self._showTrace("签到时发生未知错误 !")
|
||||
return False
|
||||
result_message = result_message_element.text
|
||||
if "签到成功" in result_message:
|
||||
try:
|
||||
detail_elements = self.__driver.find_elements(
|
||||
By.CSS_SELECTOR, ".resultMessage dd"
|
||||
)
|
||||
except:
|
||||
pass
|
||||
if detail_elements:
|
||||
details = [element.text for element in detail_elements if element.text.strip()]
|
||||
if len(details) >= 5:
|
||||
self._showTrace(f"\n"\
|
||||
f" 签到成功 !\n"\
|
||||
f" {details[1]}\n"\
|
||||
f" {details[2]}\n"\
|
||||
f" {details[3]}\n"\
|
||||
f" {details[4]}"
|
||||
)
|
||||
else:
|
||||
self._showTrace(f"\n"\
|
||||
" 签到成功 !\n"\
|
||||
" 未获取到签到详情 !"
|
||||
)
|
||||
ok_btn.click()
|
||||
return True
|
||||
else:
|
||||
failure_reason = result_message.replace("签到失败", "").strip()
|
||||
self._showTrace(f"\n"\
|
||||
" 签到失败 !\n"\
|
||||
f" {failure_reason}"
|
||||
)
|
||||
ok_btn.click()
|
||||
return False
|
||||
|
||||
|
||||
def checkin(
|
||||
self,
|
||||
username: str
|
||||
) -> bool:
|
||||
|
||||
if self.__driver is None:
|
||||
self._showTrace("未提供有效 WebDriver 实例 !")
|
||||
return False
|
||||
try:
|
||||
checkin_btn = WebDriverWait(self.__driver, 2).until(
|
||||
EC.element_to_be_clickable((By.ID, "btnCheckIn"))
|
||||
)
|
||||
except:
|
||||
self._showTrace(f"用户 {username} 签到界面加载失败 !")
|
||||
return False
|
||||
if "disabled" in checkin_btn.get_attribute("class"):
|
||||
self._showTrace("签到按钮不可用, 可能不在场馆内, 请连接图书馆网络后重试")
|
||||
return False
|
||||
checkin_btn.click()
|
||||
if self._waitResponseLoad():
|
||||
self._showTrace(f"用户 {username} 签到成功 !")
|
||||
return True
|
||||
else:
|
||||
self._showTrace(f"用户 {username} 签到失败 !")
|
||||
return False
|
||||
@@ -16,7 +16,7 @@ from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
|
||||
from LibOperator import LibOperator
|
||||
from base.LibOperator import LibOperator
|
||||
|
||||
|
||||
class LibCheckout(LibOperator):
|
||||
@@ -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)
|
||||
@@ -34,7 +34,7 @@ class LibCheckout(LibOperator):
|
||||
|
||||
|
||||
def _waitResponseLoad(
|
||||
self,
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
pass
|
||||
@@ -17,7 +17,7 @@ from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
|
||||
from LibOperator import LibOperator
|
||||
from base.LibOperator import LibOperator
|
||||
|
||||
|
||||
class LibLogin(LibOperator):
|
||||
@@ -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)
|
||||
@@ -36,30 +36,30 @@ class LibLogin(LibOperator):
|
||||
|
||||
|
||||
def _waitResponseLoad(
|
||||
self,
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
# wait to verify login success
|
||||
try:
|
||||
WebDriverWait(self.__driver, 5).until( # title contains "自选座位 :: 座位预约系统"
|
||||
WebDriverWait(self.__driver, 2).until( # title contains "自选座位 :: 座位预约系统"
|
||||
EC.title_contains("自选座位 :: 座位预约系统")
|
||||
)
|
||||
WebDriverWait(self.__driver, 3).until( # search button presence
|
||||
WebDriverWait(self.__driver, 2).until( # search button presence
|
||||
EC.presence_of_element_located((By.ID, "search"))
|
||||
)
|
||||
WebDriverWait(self.__driver, 3).until( # select content presence
|
||||
WebDriverWait(self.__driver, 2).until( # select content presence
|
||||
EC.presence_of_element_located((By.CLASS_NAME, "selectContent"))
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
self._showTrace(f"登录页面加载失败 ! : {e}")
|
||||
except:
|
||||
self._showTrace(f"登录页面加载失败 ! : 用户账号或者密码错误/验证码错误, 具体以页面提示为准")
|
||||
return False
|
||||
|
||||
|
||||
def __fillLogInElements(
|
||||
self,
|
||||
username: str,
|
||||
password: str,
|
||||
password: str
|
||||
) -> bool:
|
||||
|
||||
# ensure elements presence and fill them
|
||||
@@ -77,7 +77,7 @@ class LibLogin(LibOperator):
|
||||
|
||||
|
||||
def __autoRecognizeCaptcha(
|
||||
self,
|
||||
self
|
||||
) -> str:
|
||||
|
||||
# auto recognize captcha
|
||||
@@ -88,36 +88,34 @@ 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 ""
|
||||
|
||||
|
||||
def __manualRecognizeCaptcha(
|
||||
self,
|
||||
self
|
||||
) -> str:
|
||||
|
||||
# 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 ""
|
||||
|
||||
|
||||
def __refreshCaptcha(
|
||||
self,
|
||||
self
|
||||
):
|
||||
|
||||
# refresh captcha
|
||||
@@ -126,11 +124,45 @@ 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
|
||||
|
||||
|
||||
def __solveCaptcha(
|
||||
self,
|
||||
auto_captcha: bool = True
|
||||
) -> str:
|
||||
|
||||
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()
|
||||
else:
|
||||
self._showTrace(f"用户未配置自动识别验证码, 请手动输入验证码 !")
|
||||
captcha_text = self.__manualRecognizeCaptcha()
|
||||
if captcha_text:
|
||||
return captcha_text
|
||||
else:
|
||||
if not self.__refreshCaptcha():
|
||||
return ""
|
||||
self._showTrace(f"验证码识别失败 {max_attempts} 次, 达到最大尝试次数 !")
|
||||
return ""
|
||||
|
||||
|
||||
def __fillCaptchaElement(
|
||||
self,
|
||||
captcha_text: str
|
||||
) -> bool:
|
||||
|
||||
try:
|
||||
captcha_element = self.__driver.find_element(By.NAME, "answer")
|
||||
captcha_element.clear()
|
||||
captcha_element.send_keys(captcha_text)
|
||||
return True
|
||||
except Exception as e:
|
||||
self._showTrace(f"验证码填写失败 ! : {e}")
|
||||
return False
|
||||
|
||||
|
||||
@@ -139,7 +171,7 @@ class LibLogin(LibOperator):
|
||||
username: str,
|
||||
password: str,
|
||||
max_attempts: int = 5,
|
||||
auto_captcha: bool = True,
|
||||
auto_captcha: bool = True
|
||||
) -> bool:
|
||||
|
||||
if self.__driver is None:
|
||||
@@ -153,17 +185,11 @@ class LibLogin(LibOperator):
|
||||
password,
|
||||
):
|
||||
continue
|
||||
while True:
|
||||
if auto_captcha:
|
||||
captcha_text = self.__autoRecognizeCaptcha()
|
||||
else:
|
||||
self._showTrace(f"用户未配置自动识别验证码, 请手动输入验证码 !")
|
||||
captcha_text = self.__manualRecognizeCaptcha()
|
||||
if captcha_text:
|
||||
break
|
||||
captcha_element = self.__driver.find_element(By.NAME, "answer")
|
||||
captcha_element.clear()
|
||||
captcha_element.send_keys(captcha_text)
|
||||
captcha_text = self.__solveCaptcha(auto_captcha)
|
||||
if not captcha_text:
|
||||
continue
|
||||
if not self.__fillCaptchaElement(captcha_text):
|
||||
continue
|
||||
self._showTrace("尝试登录...")
|
||||
try:
|
||||
self.__driver.find_element(
|
||||
@@ -171,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} 次登录成功 !")
|
||||
@@ -13,7 +13,7 @@ from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
|
||||
from LibOperator import LibOperator
|
||||
from base.LibOperator import LibOperator
|
||||
|
||||
|
||||
class LibLogout(LibOperator):
|
||||
@@ -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)
|
||||
@@ -31,7 +31,7 @@ class LibLogout(LibOperator):
|
||||
|
||||
|
||||
def _waitResponseLoad(
|
||||
self,
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
return True
|
||||
@@ -39,24 +39,18 @@ class LibLogout(LibOperator):
|
||||
|
||||
def logout(
|
||||
self,
|
||||
username: str,
|
||||
username: str
|
||||
) -> bool:
|
||||
|
||||
if self.__driver is None:
|
||||
self._showTrace("未提供有效 WebDriver 实例 !")
|
||||
|
||||
return False
|
||||
|
||||
try:
|
||||
self.__driver.find_element(
|
||||
By.XPATH, "//a[@href='/logout']"
|
||||
).click()
|
||||
|
||||
self._showTrace(f"用户 {username} 注销成功 !")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self._showTrace(f"用户 {username} 注销失败 ! : {e}")
|
||||
|
||||
return False
|
||||
@@ -0,0 +1,216 @@
|
||||
# -*- 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 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
|
||||
|
||||
|
||||
class LibRenew(LibOperator):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
input_queue: queue.Queue,
|
||||
output_queue: queue.Queue,
|
||||
driver: any
|
||||
):
|
||||
|
||||
super().__init__(input_queue, output_queue)
|
||||
|
||||
self.__driver = driver
|
||||
|
||||
|
||||
def _waitResponseLoad(
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
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
|
||||
@@ -16,7 +16,7 @@ from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
|
||||
from LibOperator import LibOperator
|
||||
from base.LibOperator import LibOperator
|
||||
|
||||
|
||||
class LibReserve(LibOperator):
|
||||
@@ -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)
|
||||
@@ -55,13 +55,13 @@ class LibReserve(LibOperator):
|
||||
) -> bool:
|
||||
|
||||
try:
|
||||
WebDriverWait(self.__driver, 5).until(
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.presence_of_element_located((By.CLASS_NAME, "layoutSeat"))
|
||||
)
|
||||
title_elements = []
|
||||
# reserve failed without title elements, so we need to try
|
||||
try:
|
||||
WebDriverWait(self.__driver, 1).until(
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.presence_of_element_located((By.CSS_SELECTOR, ".layoutSeat dt"))
|
||||
)
|
||||
title_elements = self.__driver.find_elements(
|
||||
@@ -83,20 +83,18 @@ class LibReserve(LibOperator):
|
||||
raise
|
||||
if "预定好了" in title or "预约成功" in title or "操作成功" in title:
|
||||
if len(contents) >= 6:
|
||||
date_val = contents[1].split(" : ")[1].strip() if " : " in contents[1] else contents[1].strip()
|
||||
time_val = contents[2].split(" : ")[1].strip() if " : " in contents[2] else contents[2].strip()
|
||||
seat_val = contents[3].split(" : ")[1].strip() if " : " in contents[3] else contents[3].strip()
|
||||
checkin_val = contents[5].strip()
|
||||
self._showTrace(f"\n"\
|
||||
f" 预约成功 !\n"\
|
||||
f" 预约日期: {date_val}, \n"\
|
||||
f" 预约时间: {time_val}, \n"\
|
||||
f" 预约座位: {seat_val}, \n"\
|
||||
f" 签到时间: {checkin_val}")
|
||||
f" {contents[1]}\n"\
|
||||
f" {contents[2]}\n"\
|
||||
f" {contents[3]}\n"\
|
||||
f" 签到时间 :{contents[5]}"
|
||||
)
|
||||
else:
|
||||
self._showTrace(f"\n"\
|
||||
f" 预约成功 !\n"\
|
||||
f" 未找获取到详细信息")
|
||||
self._showTrace("\n"\
|
||||
" 预约成功 !\n"\
|
||||
" 未找获取到详细信息"
|
||||
)
|
||||
return True
|
||||
except:
|
||||
self._showTrace(f"预约结果加载失败 !")
|
||||
@@ -119,35 +117,41 @@ class LibReserve(LibOperator):
|
||||
return f"{hour:02d}:{minute:02d}"
|
||||
|
||||
|
||||
def __checkReserveInfo(
|
||||
def __containRequiredInfo(
|
||||
self,
|
||||
reserve_info: dict
|
||||
) -> bool:
|
||||
|
||||
try:
|
||||
# check the required information
|
||||
# reserve_info["place"]
|
||||
if reserve_info.get("floor") is None:
|
||||
# must contain the required infomation
|
||||
if reserve_info.get("floor") is None: # if existence ?
|
||||
raise ValueError("未指定楼层")
|
||||
if reserve_info["floor"] not in self.__floor_map:
|
||||
raise ValueError(f"楼层 '{reserve_info['floor']}' 不存在")
|
||||
if reserve_info["floor"] not in self.__floor_map: # if in the mao ?
|
||||
raise ValueError(f"该楼层 '{reserve_info['floor']}' 不存在")
|
||||
if reserve_info.get("room") is None:
|
||||
raise ValueError("未指定房间")
|
||||
if reserve_info["room"] not in self.__room_map:
|
||||
raise ValueError(f"房间 '{reserve_info['room']}' 不存在")
|
||||
raise ValueError(f"该房间 '{reserve_info['room']}' 不存在")
|
||||
if reserve_info.get("seat_id") is None:
|
||||
raise ValueError("未指定座位")
|
||||
if reserve_info["seat_id"] == "":
|
||||
raise ValueError("未指定座位号")
|
||||
return True
|
||||
except ValueError as e:
|
||||
self._showTrace(
|
||||
f"预约信息错误 ! : {e}, "\
|
||||
f"由于缺少必要的预约信息, 无法开始预约流程, 请检查预约信息是否完整"
|
||||
f"由于缺少必要的预约信息, 无法开始预约流程, 请检查预约信息是否完整"
|
||||
)
|
||||
return False
|
||||
|
||||
# check and try to fix the time errors
|
||||
cur_time_str = time.strftime("%Y-%m-%d %H:%M", time.localtime())
|
||||
cur_date, curr_time = cur_time_str.split()
|
||||
if not reserve_info.get("date"):
|
||||
|
||||
def __isValidDate(
|
||||
self,
|
||||
reserve_info: dict
|
||||
) -> bool:
|
||||
|
||||
cur_date = time.strftime("%Y-%m-%d", time.localtime())
|
||||
if reserve_info.get("date") is None:
|
||||
reserve_info["date"] = cur_date
|
||||
self._showTrace(f"预约日期未指定, 自动设置为当前日期: {cur_date}")
|
||||
else:
|
||||
@@ -157,96 +161,135 @@ class LibReserve(LibOperator):
|
||||
f"{reserve_info['date']} 早于当前日期 {cur_date}, 自动设置为当前日期"
|
||||
)
|
||||
reserve_info["date"] = cur_date
|
||||
# check the begin time
|
||||
begin_time = reserve_info.get("begin_time")
|
||||
if not begin_time:
|
||||
reserve_info["begin_time"] = {
|
||||
"time": curr_time,
|
||||
"max_diff": 30,
|
||||
"prefer_early": True
|
||||
}
|
||||
self._showTrace(f"开始时间未指定, 自动设置为当前时间: {curr_time}, 最大时间差为 30 分钟, 优先选择更早预约时间")
|
||||
else:
|
||||
begin_time = reserve_info["begin_time"]
|
||||
if "time" not in begin_time:
|
||||
begin_time["time"] = curr_time
|
||||
self._showTrace(f"开始时间未指定, 自动设置为当前时间: {curr_time}")
|
||||
if "max_diff" not in begin_time:
|
||||
begin_time["max_diff"] = 30
|
||||
self._showTrace(f"最大时间差未指定, 自动设置为 30 分钟")
|
||||
if "prefer_early" not in begin_time:
|
||||
begin_time["prefer_early"] = True
|
||||
self._showTrace(f"是否优先选择更早预约时间未指定, 自动设置为 True")
|
||||
expect_duration = reserve_info.get("expect_duration")
|
||||
if not expect_duration:
|
||||
reserve_info["expect_duration"] = 4
|
||||
expect_duration = 4
|
||||
self._showTrace("预约持续时间未指定, 使用默认时长为 4 小时")
|
||||
if not reserve_info.get("satisfy_duration"):
|
||||
return True
|
||||
|
||||
|
||||
def __isValidBeginTime(
|
||||
self,
|
||||
reserve_info: dict
|
||||
) -> bool:
|
||||
|
||||
cur_time = time.strftime("%H:%M", time.localtime())
|
||||
if reserve_info.get("begin_time") is None:
|
||||
reserve_info["begin_time"] = {}
|
||||
if "time" not in reserve_info["begin_time"]:
|
||||
reserve_info["begin_time"]["time"] = cur_time
|
||||
self._showTrace(f"开始时间未指定, 自动设置为当前时间: {cur_time}")
|
||||
if "max_diff" not in reserve_info["begin_time"]:
|
||||
reserve_info["begin_time"]["max_diff"] = 30
|
||||
self._showTrace(f"开始时间最大时间差未指定, 自动设置为 30 分钟")
|
||||
if "prefer_early" not in reserve_info["begin_time"]:
|
||||
reserve_info["begin_time"]["prefer_early"] = True
|
||||
self._showTrace(f"是否优先选择更早开始时间未指定, 自动设置为 True")
|
||||
return True
|
||||
|
||||
|
||||
def __isValidExpectDuration(
|
||||
self,
|
||||
reserve_info: dict
|
||||
) -> bool:
|
||||
|
||||
if reserve_info.get("satisfy_duration") is None:
|
||||
reserve_info["satisfy_duration"] = True
|
||||
self._showTrace("预约满足时长要求未指定, 默认满足")
|
||||
# check the end time
|
||||
if not reserve_info.get("end_time"):
|
||||
begin_mins = self.__timeToMins(reserve_info["begin_time"]["time"])
|
||||
end_mins = begin_mins + reserve_info["expect_duration"] * 60
|
||||
end_time_str = self.__minsToTime(end_mins)
|
||||
if reserve_info["satisfy_duration"]:
|
||||
if reserve_info.get("expect_duration") is None:
|
||||
reserve_info["expect_duration"] = 4
|
||||
self._showTrace("需要满足预约持续时间, 但未指定, 使用默认时长为 4 小时")
|
||||
return True
|
||||
|
||||
|
||||
def __isValidEndTime(
|
||||
self,
|
||||
reserve_info: dict
|
||||
) -> bool:
|
||||
|
||||
if reserve_info.get("end_time") is None:
|
||||
reserve_info["end_time"] = {}
|
||||
if "time" not in reserve_info["end_time"]:
|
||||
end_mins = self.__timeToMins(reserve_info["begin_time"]["time"])
|
||||
end_mins = end_mins + int(reserve_info["expect_duration"]*60)
|
||||
reserve_info["end_time"] = {
|
||||
"time": end_time_str,
|
||||
"time": self.__minsToTime(end_mins),
|
||||
"max_diff": 30,
|
||||
"prefer_early": False
|
||||
}
|
||||
self._showTrace(f"结束时间未指定, 自动设置为开始时间加上期望时长: {end_time_str}, 最大时间差为 30 分钟, 优先选择较晚预约时间")
|
||||
self._showTrace(
|
||||
f"结束时间未指定, 自动设置为开始时间加上期望时长: {reserve_info['end_time']['time']}"
|
||||
)
|
||||
if "max_diff" not in reserve_info["end_time"]:
|
||||
reserve_info["end_time"]["max_diff"] = 30
|
||||
self._showTrace(f"结束时间最大时间差未指定, 自动设置为 30 分钟")
|
||||
if "prefer_early" not in reserve_info["end_time"]:
|
||||
reserve_info["end_time"]["prefer_early"] = False
|
||||
self._showTrace(f"是否优先选择较晚结束时间未指定, 自动设置为 True")
|
||||
return True
|
||||
|
||||
|
||||
def __finalCheck(
|
||||
self,
|
||||
reserve_info: dict
|
||||
):
|
||||
|
||||
begin_time, end_time = reserve_info["begin_time"], reserve_info["end_time"]
|
||||
begin_mins = self.__timeToMins(begin_time["time"])
|
||||
end_mins = self.__timeToMins(end_time["time"])
|
||||
# if end time is earlier than begin_time, exchange them
|
||||
if end_mins < begin_mins:
|
||||
self._showTrace(
|
||||
f"结束时间 {end_time['time']} 早于开始时间 {begin_time['time']}, 尝试交换时间"
|
||||
)
|
||||
reserve_info["end_time"] = begin_time
|
||||
reserve_info["begin_time"] = end_time
|
||||
begin_time, end_time = reserve_info["begin_time"], reserve_info["end_time"]
|
||||
begin_mins = self.__timeToMins(begin_time["time"])
|
||||
end_mins = self.__timeToMins(end_time["time"])
|
||||
# ensure the end time is not later than 23:30
|
||||
if end_mins > self.__timeToMins("23:30"):
|
||||
self._showTrace(
|
||||
f"结束时间 {end_time['time']} 晚于 23:30, 自动设置为 23:30"
|
||||
)
|
||||
reserve_info["end_time"]["time"] = "23:30"
|
||||
end_mins = self.__timeToMins("23:30")
|
||||
# ensure the duration is not longer than 8 hours
|
||||
if reserve_info["satisfy_duration"]:
|
||||
if reserve_info["expect_duration"] > 8:
|
||||
self._showTrace(
|
||||
f"该用户设置了优先满足时长要求, 但是预约期望持续时间 "
|
||||
f"{reserve_info['expect_duration']} 小时 "
|
||||
f"超出最大时长 8 小时, 自动设置为 8 小时"
|
||||
)
|
||||
reserve_info["expect_duration"] = 8
|
||||
else:
|
||||
end_time = reserve_info["end_time"]
|
||||
if "time" not in end_time:
|
||||
begin_mins = self.__timeToMins(reserve_info["begin_time"]["time"])
|
||||
end_mins = begin_mins + reserve_info["expect_duration"] * 60
|
||||
end_time["time"] = self.__minsToTime(end_mins)
|
||||
self._showTrace(f"结束时间未指定, 自动设置为开始时间加上期望时长: {end_time['time']}")
|
||||
if "max_diff" not in end_time:
|
||||
end_time["max_diff"] = 30
|
||||
self._showTrace(f"最大时间差未指定, 自动设置为 30 分钟")
|
||||
if "prefer_early" not in end_time:
|
||||
end_time["prefer_early"] = False
|
||||
self._showTrace(f"是否优先选择较早预约时间未指定, 自动设置为 False")
|
||||
# check the reserve time boundary and fix the errors
|
||||
#
|
||||
# get time string for message show
|
||||
begin_time_str = reserve_info["begin_time"]["time"]
|
||||
end_time_str = reserve_info["end_time"]["time"]
|
||||
if end_mins - begin_mins > 8*60:
|
||||
self._showTrace(
|
||||
f"该用户未设置优先满足时长要求, 但是检查到预约持续时间 "
|
||||
f"{float((end_mins - begin_mins)/60)} 小时 "
|
||||
f"超出最大时长 8 小时, 自动设置为 8 小时"
|
||||
)
|
||||
reserve_info["end_time"]["time"] = self.__minsToTime(begin_mins + 8*60)
|
||||
return True
|
||||
|
||||
# minute time for check and fix them
|
||||
begin_mins = self.__timeToMins(begin_time_str)
|
||||
end_mins = self.__timeToMins(end_time_str)
|
||||
|
||||
# ensure begin time is not later than end time
|
||||
if begin_mins > end_mins:
|
||||
reserve_info["begin_time"]["time"], reserve_info["end_time"]["time"] = end_time_str, begin_time_str
|
||||
reserve_info["begin_time"]["prefer_early"], reserve_info["end_time"]["prefer_early"] = \
|
||||
reserve_info["end_time"]["prefer_early"], reserve_info["begin_time"]["prefer_early"]
|
||||
self._showTrace("预约开始时间晚于预约结束时间,自动调换开始时间和结束时间")
|
||||
def __checkReserveInfo(
|
||||
self,
|
||||
reserve_info: dict
|
||||
) -> bool:
|
||||
|
||||
# update the begin_mins and end_mins after swap
|
||||
begin_time_str, end_time_str = end_time_str, begin_time_str
|
||||
begin_mins, end_mins = end_mins, begin_mins
|
||||
|
||||
# ensure end time is not later than 22:30
|
||||
max_end_mins = self.__timeToMins("22:30")
|
||||
if end_mins > max_end_mins:
|
||||
reserve_info["end_time"]["time"] = "22:30"
|
||||
end_time_str = "22:30"
|
||||
end_mins = max_end_mins
|
||||
self._showTrace("预约结束时间超过 22:30, 自动设置为 22:30")
|
||||
|
||||
# ensure expect duration is shorter than 8 hours
|
||||
max_duration_mins = 8 * 60
|
||||
duration_mins = end_mins - begin_mins
|
||||
if duration_mins > max_duration_mins:
|
||||
new_end_mins = begin_mins + max_duration_mins
|
||||
reserve_info["end_time"]["time"] = self.__minsToTime(new_end_mins)
|
||||
self._showTrace("预约持续时间超过8小时, 自动设置为 8 小时")
|
||||
if not self.__containRequiredInfo(reserve_info):
|
||||
return False
|
||||
if not self.__isValidDate(reserve_info):
|
||||
return False
|
||||
if not self.__isValidBeginTime(reserve_info):
|
||||
return False
|
||||
if not self.__isValidExpectDuration(reserve_info):
|
||||
return False
|
||||
if not self.__isValidEndTime(reserve_info):
|
||||
return False
|
||||
if not self.__finalCheck(reserve_info):
|
||||
return False
|
||||
self._showTrace(
|
||||
f"预约信息检查完成,准备预约 "
|
||||
f"预约信息检查完成, 准备预约 "
|
||||
f"{reserve_info['date']} "
|
||||
f"{reserve_info['begin_time']["time"]} - "
|
||||
f"{reserve_info['end_time']["time"]} "
|
||||
@@ -263,17 +306,17 @@ class LibReserve(LibOperator):
|
||||
trigger_locator: tuple,
|
||||
fail_msg: str,
|
||||
success_msg: str,
|
||||
option_locator: tuple = None,
|
||||
option_locator: tuple = None
|
||||
) -> bool:
|
||||
|
||||
try:
|
||||
# click the trigger element
|
||||
WebDriverWait(self.__driver, 5).until(
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.element_to_be_clickable(trigger_locator)
|
||||
).click()
|
||||
if option_locator:
|
||||
# select the option element if specified
|
||||
WebDriverWait(self.__driver, 5).until(
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.element_to_be_clickable(option_locator)
|
||||
).click()
|
||||
self._showTrace(success_msg)
|
||||
@@ -283,11 +326,52 @@ class LibReserve(LibOperator):
|
||||
return False
|
||||
|
||||
|
||||
def __clickElementByJS(
|
||||
self,
|
||||
trigger_locator_id: str,
|
||||
option_query_selector: str,
|
||||
fail_msg: str,
|
||||
success_msg: str,
|
||||
) -> bool:
|
||||
|
||||
script = f"""
|
||||
try {{
|
||||
var trigger = document.getElementById('{trigger_locator_id}');
|
||||
if (trigger) {{
|
||||
trigger.click();
|
||||
var option = document.querySelector("{option_query_selector}");
|
||||
if (option) {{
|
||||
option.click();
|
||||
return true;
|
||||
}}
|
||||
return false;
|
||||
}}
|
||||
return false;
|
||||
}} catch (e) {{
|
||||
return false;
|
||||
}}
|
||||
"""
|
||||
result = self.__driver.execute_script(script)
|
||||
time.sleep(0.1)
|
||||
if result:
|
||||
self._showTrace(success_msg)
|
||||
else:
|
||||
self._showTrace(fail_msg)
|
||||
return result
|
||||
|
||||
|
||||
def __selectDate(
|
||||
self,
|
||||
date_str: str
|
||||
) -> bool:
|
||||
|
||||
if self.__clickElementByJS(
|
||||
trigger_locator_id="onDate_select",
|
||||
option_query_selector=f"p#options_onDate a[value='{date_str}']",
|
||||
success_msg=f"日期 {date_str} 选择成功 !",
|
||||
fail_msg=f"选择日期失败 ! : {date_str} 不可用"
|
||||
):
|
||||
return True
|
||||
return self.__clickElement(
|
||||
trigger_locator=(By.ID, "onDate_select"),
|
||||
option_locator=(By.XPATH, f"//p[@id='options_onDate']/a[@value='{date_str}']"),
|
||||
@@ -301,12 +385,20 @@ class LibReserve(LibOperator):
|
||||
place: str
|
||||
) -> bool:
|
||||
|
||||
actual_place = "1" if place == "图书馆" else "1"
|
||||
place = "1" # the library only have this place :)
|
||||
display_place = "图书馆"
|
||||
if self.__clickElementByJS(
|
||||
trigger_locator_id="display_building",
|
||||
option_query_selector=f"p#options_building a[value='{place}']",
|
||||
success_msg=f"预约场所 {display_place} 选择成功 !",
|
||||
fail_msg=f"选择预约场所失败 ! : {display_place} 不可用"
|
||||
):
|
||||
return True
|
||||
return self.__clickElement(
|
||||
trigger_locator=(By.ID, "display_building"),
|
||||
option_locator=(By.XPATH, f"//p[@id='options_building']/a[@value='{actual_place}']"),
|
||||
success_msg=f"预约场所 {place} 选择成功 !",
|
||||
fail_msg=f"选择预约场所失败 ! : {place} 不可用"
|
||||
option_locator=(By.XPATH, f"//p[@id='options_building']/a[@value='{place}']"),
|
||||
success_msg=f"预约场所 {display_place} 选择成功 !",
|
||||
fail_msg=f"选择预约场所失败 ! : {display_place} 不可用"
|
||||
)
|
||||
|
||||
|
||||
@@ -316,6 +408,13 @@ class LibReserve(LibOperator):
|
||||
) -> bool:
|
||||
|
||||
display_floor = self.__floor_map.get(floor)
|
||||
if self.__clickElementByJS(
|
||||
trigger_locator_id="floor_select",
|
||||
option_query_selector=f"p#options_floor a[value='{floor}']",
|
||||
success_msg=f"楼层 {display_floor} 选择成功 !",
|
||||
fail_msg=f"选择楼层失败 ! : {display_floor} 不可用"
|
||||
):
|
||||
return True
|
||||
return self.__clickElement(
|
||||
trigger_locator=(By.ID, "floor_select"),
|
||||
option_locator=(By.XPATH, f"//p[@id='options_floor']/a[@value='{floor}']"),
|
||||
@@ -330,12 +429,24 @@ class LibReserve(LibOperator):
|
||||
) -> bool:
|
||||
|
||||
display_room = self.__room_map.get(room)
|
||||
return self.__clickElement(
|
||||
trigger_locator=(By.ID, f"room_{room}"),
|
||||
option_locator=None,
|
||||
success_msg=f"房间 {display_room} 选择成功 !",
|
||||
fail_msg=f"选择房间失败 ! : {display_room} 不可用"
|
||||
)
|
||||
# find room
|
||||
try:
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.element_to_be_clickable((By.ID, "findRoom"))
|
||||
).click()
|
||||
except:
|
||||
self._showTrace("加载房间/区域失败 !")
|
||||
return False
|
||||
# select room
|
||||
try:
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.element_to_be_clickable((By.ID, f"room_{room}"))
|
||||
).click()
|
||||
self._showTrace(f"房间 {display_room} 选择成功 !")
|
||||
return True
|
||||
except:
|
||||
self._showTrace(f"选择房间失败 ! : {display_room} 不可用")
|
||||
return False
|
||||
|
||||
|
||||
def __selectSeat(
|
||||
@@ -345,9 +456,16 @@ class LibReserve(LibOperator):
|
||||
|
||||
try:
|
||||
# wait fot seat layout element to load
|
||||
WebDriverWait(self.__driver, 5).until(
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.presence_of_element_located((By.ID, "seatLayout"))
|
||||
)
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.presence_of_all_elements_located((By.CSS_SELECTOR, "li[id^='seat_']"))
|
||||
)
|
||||
except:
|
||||
self._showTrace(f"座位加载失败 !")
|
||||
return False
|
||||
try:
|
||||
all_seats = self.__driver.find_elements(
|
||||
By.CSS_SELECTOR, "li[id^='seat_']"
|
||||
)
|
||||
@@ -356,7 +474,7 @@ class LibReserve(LibOperator):
|
||||
if not seat_id_upper == seat.text.lstrip('0'):
|
||||
continue
|
||||
seat_link = seat.find_element(By.TAG_NAME, "a")
|
||||
WebDriverWait(self.__driver, 5).until(
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.element_to_be_clickable(seat_link)
|
||||
)
|
||||
seat_link.click()
|
||||
@@ -378,6 +496,15 @@ class LibReserve(LibOperator):
|
||||
prefer_earlier: bool = True
|
||||
) -> int:
|
||||
|
||||
try:
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.presence_of_all_elements_located(
|
||||
(By.CSS_SELECTOR, f"#{time_id} ul li a")
|
||||
)
|
||||
)
|
||||
except:
|
||||
self._showTrace(f"{time_type} 选择失败 ! : 当前未查询到可用时间")
|
||||
return -1
|
||||
try:
|
||||
all_time_opts = self.__driver.find_elements(
|
||||
By.CSS_SELECTOR,
|
||||
@@ -388,6 +515,9 @@ class LibReserve(LibOperator):
|
||||
best_actual_diff = None
|
||||
best_time_opt = None
|
||||
|
||||
if not all_time_opts:
|
||||
self._showTrace(f"{time_type} 选择失败 ! : 当前未查询到可用时间")
|
||||
return -1
|
||||
for time_opt in all_time_opts:
|
||||
time_attr = time_opt.get_attribute("time")
|
||||
if time_attr == "now":
|
||||
@@ -449,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
|
||||
@@ -462,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)}"
|
||||
@@ -489,102 +627,9 @@ class LibReserve(LibOperator):
|
||||
return True
|
||||
|
||||
|
||||
def canReserve(
|
||||
self,
|
||||
date: str
|
||||
) -> bool:
|
||||
|
||||
if date is None:
|
||||
self._showTrace("日期未指定, 无法检查预约状态")
|
||||
return True
|
||||
else:
|
||||
self._showTrace(f"正在检查用户在日期 {date} 是否可预约......")
|
||||
date_obj = datetime.strptime(date, "%Y-%m-%d").date()
|
||||
try:
|
||||
# we need to navigate to the history page to check if we can reserve
|
||||
WebDriverWait(self.__driver, 5).until(
|
||||
EC.element_to_be_clickable((By.XPATH, "//a[@href='/history?type=SEAT']"))
|
||||
).click()
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.presence_of_element_located((By.CLASS_NAME, "myReserveList"))
|
||||
)
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.presence_of_element_located((By.CSS_SELECTOR, ".myReserveList dl"))
|
||||
)
|
||||
except:
|
||||
self._showTrace("加载预约记录页面失败 !")
|
||||
return False
|
||||
checked_count = 0
|
||||
max_attemots = 3 # we only check (3*4=)12 reservations
|
||||
|
||||
for _ in range(max_attemots):
|
||||
try:
|
||||
# check if there's any reservation on the date
|
||||
reservations = self.__driver.find_elements(
|
||||
By.CSS_SELECTOR, ".myReserveList dl"
|
||||
)
|
||||
except:
|
||||
self._showTrace("加载预约记录失败 !")
|
||||
return False
|
||||
for i in range(checked_count, len(reservations) - 1): # the last one is load button
|
||||
reservation = reservations[i]
|
||||
try:
|
||||
time_element = reservation.find_element(
|
||||
By.CSS_SELECTOR, "dt"
|
||||
)
|
||||
status_elements = reservation.find_elements(
|
||||
By.CSS_SELECTOR, "a"
|
||||
)
|
||||
is_reserved = any("已预约" in status.text for status in status_elements)
|
||||
# process time element to get the date string
|
||||
time_str = time_element.text.strip()
|
||||
today = datetime.now().date()
|
||||
if "明天" in time_str:
|
||||
target_date = today + timedelta(days=1)
|
||||
date_str = target_date.strftime("%Y-%m-%d")
|
||||
elif "今天" in time_str:
|
||||
target_date = today
|
||||
date_str = target_date.strftime("%Y-%m-%d")
|
||||
elif "昨天" in time_str:
|
||||
target_date = today - timedelta(days=1)
|
||||
date_str = target_date.strftime("%Y-%m-%d")
|
||||
else:
|
||||
date_match = re.search(r'(\d{4}-\d{1,2}-\d{1,2})', time_str)
|
||||
if date_match:
|
||||
date_str = date_match.group(1)
|
||||
else:
|
||||
continue
|
||||
# reservation is earlier than the given date, can reserve
|
||||
if datetime.strptime(date_str, "%Y-%m-%d").date() < date_obj:
|
||||
self._showTrace(f"用户在 {date} 可预约")
|
||||
return True
|
||||
# reservation is later than the given date, check the next one
|
||||
elif datetime.strptime(date_str, "%Y-%m-%d").date() > date_obj:
|
||||
continue
|
||||
# compare with the given date
|
||||
if date_str == date and is_reserved:
|
||||
self._showTrace(f"用户在 {date} 已存在有效预约, 无法预约")
|
||||
return False
|
||||
except:
|
||||
self._showTrace(f"解析第 {i + 1} 条预约记录时发生未知错误 !")
|
||||
continue
|
||||
checked_count = len(reservations) - 1
|
||||
# load new reservations if still not sure
|
||||
try:
|
||||
more_btn = self.__driver.find_element(By.ID, "moreBtn")
|
||||
if more_btn.is_displayed() and more_btn.is_enabled():
|
||||
self.__driver.execute_script("arguments[0].scrollIntoView(true);", more_btn)
|
||||
self.__driver.execute_script("arguments[0].click();", more_btn)
|
||||
else:
|
||||
break
|
||||
except:
|
||||
break
|
||||
self._showTrace(f"用户在 {date} 可预约")
|
||||
return True
|
||||
|
||||
|
||||
def reserve(
|
||||
self,
|
||||
username: str,
|
||||
reserve_info: dict
|
||||
) -> bool:
|
||||
|
||||
@@ -597,31 +642,22 @@ class LibReserve(LibOperator):
|
||||
return False
|
||||
# map page
|
||||
try:
|
||||
WebDriverWait(self.__driver, 5).until(
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.element_to_be_clickable((By.XPATH, "//a[@href='/map']"))
|
||||
).click()
|
||||
WebDriverWait(self.__driver, 5).until(
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.presence_of_element_located((By.ID, "seatLayout"))
|
||||
)
|
||||
except:
|
||||
self._showTrace(f"加载预约选座页面失败 !")
|
||||
return False
|
||||
# date, place, floor
|
||||
# date, place, floor, room
|
||||
if not self.__selectDate(reserve_info["date"]):
|
||||
return False
|
||||
if not self.__selectPlace(reserve_info["place"]):
|
||||
return False
|
||||
if not self.__selectFloor(reserve_info["floor"]):
|
||||
return False
|
||||
# room find
|
||||
try:
|
||||
WebDriverWait(self.__driver, 5).until(
|
||||
EC.element_to_be_clickable((By.ID, "findRoom"))
|
||||
).click()
|
||||
except:
|
||||
self._showTrace("加载房间/区域失败 !")
|
||||
return False
|
||||
# room
|
||||
if not self.__selectRoom(reserve_info["room"]):
|
||||
return False
|
||||
else:
|
||||
@@ -638,7 +674,7 @@ class LibReserve(LibOperator):
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
WebDriverWait(self.__driver, 5).until(
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.element_to_be_clickable((By.ID, "reserveBtn"))
|
||||
).click()
|
||||
submit_reserve = True
|
||||
@@ -649,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
|
||||
@@ -0,0 +1,12 @@
|
||||
"""
|
||||
Operators module for the AutoLibrary project.
|
||||
|
||||
Here are the classes and modules in this package:
|
||||
- AutoLib: AutoLibrary operator.
|
||||
- LibLogin: Library operator for logging in.
|
||||
- LibLogout: Library operator for logging out.
|
||||
- LibReserve: Library operator for reserving seat.
|
||||
- LibCheckin: Library operator for checking in seat.
|
||||
- LibCheckout: Library operator for checking out seat.
|
||||
- LibRenew: Library operator for renewing seat.
|
||||
"""
|
||||
@@ -0,0 +1,7 @@
|
||||
"""
|
||||
Utils module for the AutoLibrary project.
|
||||
|
||||
Here are the classes and modules in this package:
|
||||
- ConfigReader: Configuration reader class for the AutoLibrary project.
|
||||
- ConfigWriter: Configuration writer class for the AutoLibrary project.
|
||||
"""
|
||||
Reference in New Issue
Block a user