├── .github
├── ISSUE_TEMPLATE
│ ├── bug-report.yml
│ └── feature-request.yml
├── pull_request_template.md
├── release-drafter.yml
└── workflows
│ ├── drawer.yml
│ └── release.yml
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── config.py
├── icon.ico
├── main.py
├── main.spec
├── requirements.txt
├── solver
├── aliSolver.py
└── tmp.html
├── tab
├── go.py
├── login.py
├── order.py
├── problems.py
└── settings.py
└── util
├── CookieManager.py
├── CppRequest.py
├── KVDatabase.py
├── PushPlusUtil.py
├── ServerChanUtil.py
├── TimeService.py
└── error.py
/.github/ISSUE_TEMPLATE/bug-report.yml:
--------------------------------------------------------------------------------
1 | name: 漏洞反馈
2 | description: 报错或漏洞请使用这个模板创建,不使用此模板创建的异常、漏洞相关issue将被直接关闭
3 | title: "[Bug]: "
4 | labels: ["bug?"]
5 | body:
6 | - type: dropdown
7 | attributes:
8 | label: 部署方式
9 | description: "主程序使用的部署方式"
10 | options:
11 | - 手动部署
12 | - release的打包文件
13 | validations:
14 | required: true
15 | - type: input
16 | attributes:
17 | label: 版本
18 | description: 下载release里面程序的版本。
19 | placeholder: 例如 v1.0.0
20 | validations:
21 | required: true
22 | - type: textarea
23 | attributes:
24 | label: 异常情况
25 | description: 完整描述异常情况,什么时候发生的、发生了什么
26 | validations:
27 | required: true
28 | - type: textarea
29 | attributes:
30 | label: 报错信息
31 | description: 请提供完整的**控制台**报错信息(若有)以及 相关的log文件(去除敏感信息)
32 | validations:
33 | required: false
34 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature-request.yml:
--------------------------------------------------------------------------------
1 | name: 需求建议
2 | title: "[Feature]: "
3 | labels: ["enhancement"]
4 | description: "新功能或现有功能优化请使用这个模板;不符合类别的issue将被直接关闭"
5 | body:
6 | - type: dropdown
7 | attributes:
8 | label: 这是一个?
9 | description: 新功能建议还是现有功能优化
10 | options:
11 | - 新功能
12 | - 现有功能优化
13 | validations:
14 | required: true
15 | - type: textarea
16 | attributes:
17 | label: 详细描述
18 | description: 详细描述,越详细越好
19 | validations:
20 | required: true
21 |
22 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | ## 概述
2 |
3 | 实现/解决/优化的内容:
4 |
5 | ### 事务
6 |
7 | - [ ] 已与维护者在issues或其他平台沟通此PR大致内容
8 |
9 | ## 以下内容可在起草PR后、合并PR前逐步完成
10 |
11 | ### 功能
12 |
13 | - [ ] 已编写完善的配置文件字段说明(若有新增)
14 | - [ ] 已编写面向用户的新功能说明(若有必要)
15 | - [ ] 已测试新功能或更改
16 |
17 | ### 兼容性
18 |
19 | - [ ] 已处理版本兼容性
20 | - [ ] 已处理插件兼容问题
21 |
22 | ### 风险
23 |
24 | 可能导致或已知的问题:
25 |
--------------------------------------------------------------------------------
/.github/release-drafter.yml:
--------------------------------------------------------------------------------
1 | name-template: 'v$RESOLVED_VERSION 🌈'
2 | tag-template: 'v$RESOLVED_VERSION'
3 | categories:
4 | - title: '🚀 Features'
5 | labels:
6 | - 'feature'
7 | - 'enhancement'
8 | - title: '🐛 Bug Fixes'
9 | labels:
10 | - 'fix'
11 | - 'bugfix'
12 | - 'bug'
13 | - title: '🧰 Maintenance'
14 | label: 'chore'
15 | change-template: '- $TITLE @$AUTHOR (#$NUMBER)'
16 | change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks.
17 | version-resolver:
18 | major:
19 | labels:
20 | - 'major'
21 | minor:
22 | labels:
23 | - 'minor'
24 | patch:
25 | labels:
26 | - 'patch'
27 | default: patch
28 | template: |
29 | ## Changes
30 | $CHANGES
--------------------------------------------------------------------------------
/.github/workflows/drawer.yml:
--------------------------------------------------------------------------------
1 | name: Release Drafter
2 |
3 | on:
4 | push:
5 | # branches to consider in the event; optional, defaults to all
6 | branches:
7 | - main
8 | # pull_request event is required only for autolabeler
9 | pull_request:
10 | # Only following types are handled by the action, but one can default to all as well
11 | types: [opened, reopened, synchronize]
12 | # pull_request_target event is required for autolabeler to support PRs from forks
13 | # pull_request_target:
14 | # types: [opened, reopened, synchronize]
15 |
16 | permissions:
17 | contents: read
18 |
19 | jobs:
20 | update_release_draft:
21 | permissions:
22 | # write permission is required to create a github release
23 | contents: write
24 | # write permission is required for autolabeler
25 | # otherwise, read permission is required at least
26 | pull-requests: write
27 | runs-on: ubuntu-latest
28 | steps:
29 | # (Optional) GitHub Enterprise requires GHE_HOST variable set
30 | #- name: Set GHE_HOST
31 | # run: |
32 | # echo "GHE_HOST=${GITHUB_SERVER_URL##https:\/\/}" >> $GITHUB_ENV
33 |
34 | # Drafts your next Release notes as Pull Requests are merged into "master"
35 | - uses: release-drafter/release-drafter@v5
36 | with:
37 | token: ${{ secrets.GITHUB_TOKEN }}
38 | # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml
39 | # with:
40 | # config-name: my-config.yml
41 | # disable-autolabeler: true
42 | env:
43 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
44 | - uses: akhilmhdh/contributors-readme-action@v2.3.10
45 | env:
46 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
47 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Python Release
2 |
3 | on:
4 | push:
5 | tags:
6 | - 'v*.*.*'
7 |
8 | jobs:
9 | build:
10 | name: Build Executables
11 | runs-on: ${{ matrix.config.os.name }}
12 | strategy:
13 | matrix:
14 | config:
15 | - os:
16 | name: ubuntu-22.04
17 | filename: linux_amd64
18 | - os:
19 | name: ubuntu-22.04-arm
20 | filename: linux_arm64
21 | - os:
22 | name: macos-14
23 | filename: macos_arm64
24 | - os:
25 | name: macos-13
26 | filename: macos_intel
27 | - os:
28 | name: windows-latest
29 | filename: windows_amd64
30 | steps:
31 | - name: Checkout source
32 | uses: actions/checkout@v2
33 |
34 |
35 | - name: Set up Python
36 | uses: actions/setup-python@v2
37 | with:
38 | python-version: 3.11
39 |
40 |
41 | - name: Upgrade setuptools, wheel, and install requirements
42 | run: |
43 | pip install --upgrade setuptools wheel pyinstaller~=5.13.2 && pip install -r requirements.txt
44 |
45 |
46 | - name: Build Pyinstaller
47 | shell: bash
48 | run: |
49 | pyinstaller main.spec
50 | - name: Zip the Build-windows
51 | if: matrix.config.os.filename == 'windows_amd64'
52 | run: Compress-Archive -Path ./dist/cppTicKerBuy.exe -DestinationPath tmp.zip
53 |
54 | - name: Zip the Build-linux
55 | if: matrix.config.os.filename != 'windows_amd64'
56 | run: |
57 | cd ./dist
58 | zip -r ../tmp.zip cppTickerBuy
59 |
60 | - name: Upload binaries to release
61 | uses: svenstaro/upload-release-action@v2
62 | with:
63 | file: tmp.zip
64 | asset_name: cppTicKerBuy_${{ matrix.config.os.filename }}_${{ github.ref_name }}.zip
65 | tag: ${{ github.ref }}
66 | overwrite: true
67 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | config
2 | venv
3 | .idea
4 | dist
5 | log/*
6 | log/log.txt
7 | build
8 | *.pyc
9 | *.log
10 | *.json
11 | tmp/*
12 | tmp
13 | __pycache__/
14 | */__pycache__/
15 | *.py[cod]
16 | *$py.class
17 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.11
2 | WORKDIR /app
3 | RUN apt-get update &&\
4 | apt-get install -y curl
5 | RUN curl -sSf https://sh.rustup.rs | sh -s -- -y
6 | ENV PATH="/root/.cargo/bin:${PATH}"
7 | ENV TZ=Asia/Shanghai
8 | COPY requirements.txt .
9 | RUN python -m pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
10 | COPY . .
11 | ENV GRADIO_SERVER_NAME="0.0.0.0"
12 | ENV GRADIO_SERVER_PORT 7860
13 | CMD ["python", "main.py"]
14 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 mikumifa
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 8
2 |
3 |
4 |
5 |
cppTickerBuy
6 |
7 | 
8 | 
9 | 
10 | 
11 |
12 |
13 |
14 | 开源免费,简单易用,图形界面, 速度极快的CPP抢票辅助工具
15 |
16 |
17 | ## 快速安装
18 |
19 | Windows 下载最新的release文件 (cppTickerBuy.zip) [下载链接](https://github.com/mikumifa/cppTickerBuy/releases)
20 | > **NOTE**
21 | >
22 | > 如果你对Github一点也不了解, 不知道在哪下载
23 | >
24 | > 这里有一份小白指南 [点我前往小白指南](https://github.com/mikumifa/biliTickerBuy/wiki/%E5%B0%8F%E7%99%BD%E4%B8%8B%E8%BD%BD%E6%8C%87%E5%8D%97)
25 |
26 | ## 使用说明书
27 | 重构了UI,启动终端第一行会显示
28 |
29 | ```
30 | Running on local URL: http://127.0.0.1:xxx
31 | ```
32 |
33 | 访问对应的网址即可
34 |
35 | 使用源码手动启动 `main.py` 时可带有如下参数:
36 |
37 | - `--share` 选择是否创建sharelink,需传入布尔值 `True/False` ,默认为 `False`
38 |
39 | 说明书暂时没时间写,用以前的项目代替一下
40 | [点我前往更加详细的使用说明书](https://github.com/mikumifa/biliTickerBuy/wiki/%E6%8A%A2%E7%A5%A8%E8%AF%B4%E6%98%8E)
41 |
42 |
43 | ## 项目问题
44 |
45 | 程序使用问题: [点此链接前往discussions](https://github.com/mikumifa/cppTickerBuy/discussions)
46 |
47 | 反馈程序BUG或者提新功能建议: [点此链接向项目提出反馈BUG](https://github.com/mikumifa/cppTickerBuy/issues/new/choose)
48 |
49 | ## 其他可用脚本
50 |
51 | | 链接 | 主要特色 |
52 | | --------------------------------------------------------- | ---------------------- |
53 | | https://github.com/Koileo/ticket_for_allcpp | 能同时开多张票 |
54 |
55 |
56 |
57 | ## 项目贡献者
58 |
59 |
60 |
80 |
81 |
82 |
83 | ## Star History
84 |
85 | [](https://star-history.com/#mikumifa/cppTickerBuy&Date)
86 |
87 | ## 免责声明
88 |
89 | 详见[MIT License](./LICENSE),切勿进行盈利,所造成的后果与本人无关。
90 |
91 |
--------------------------------------------------------------------------------
/config.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 |
4 | import loguru
5 |
6 | from util.CppRequest import CppRequest
7 | from util.KVDatabase import KVDatabase
8 | from util.TimeService import TimeService
9 |
10 |
11 |
12 |
13 |
14 | BASE_DIR = os.path.dirname(os.path.realpath(sys.executable))
15 | TEMP_PATH = os.path.join(BASE_DIR,'tmp')
16 | os.makedirs(TEMP_PATH, exist_ok=True)
17 | loguru.logger.info(f"设置路径 TEMP_PATH={TEMP_PATH} BASE_DIR={BASE_DIR}")
18 | configDB = KVDatabase(os.path.join(BASE_DIR, "config.json"))
19 | if not configDB.contains("cookie_path"):
20 | configDB.insert("cookie_path", os.path.join(BASE_DIR, "cookies.json"))
21 | main_request = CppRequest(cookies_config_path=configDB.get("cookie_path"))
22 | global_cookieManager = main_request.cookieManager
23 |
24 | ## 时间
25 | time_service = TimeService()
26 | time_service.set_timeoffset(time_service.compute_timeoffset())
27 |
--------------------------------------------------------------------------------
/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mikumifa/cppTickerBuy/370d58e201b61d3a22f1cbcbd53520bee6658880/icon.ico
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
1 | import argparse
2 |
3 | import gradio as gr
4 | from loguru import logger
5 |
6 | from tab.go import go_tab
7 | from tab.login import login_tab
8 | from tab.order import order_tab
9 | from tab.problems import problems_tab
10 | from tab.settings import setting_tab
11 |
12 | header = """
13 | # CPP 抢票🌈
14 |
15 | ⚠️此项目完全开源免费 ([项目地址](https://github.com/mikumifa/cppTickerBuy)),切勿进行盈利,所造成的后果与本人无关。
16 | """
17 |
18 | short_js = """
19 |
20 |
21 | """
22 |
23 | custom_css = """
24 | .pay_qrcode img {
25 | width: 300px !important;
26 | height: 300px !important;
27 | margin-top: 20px; /* 避免二维码头部的说明文字挡住二维码 */
28 | }
29 | """
30 |
31 | if __name__ == "__main__":
32 | parser = argparse.ArgumentParser()
33 | parser.add_argument("--port", type=int, default=7860, help="server port")
34 | parser.add_argument("--share", type=bool, default=False, help="create a public link")
35 | args = parser.parse_args()
36 |
37 | logger.add("app.log")
38 | with gr.Blocks(head=short_js, css=custom_css) as demo:
39 | gr.Markdown(header)
40 | with gr.Tab("配置"):
41 | setting_tab()
42 | with gr.Tab("抢票"):
43 | go_tab()
44 | with gr.Tab("查看订单"):
45 | order_tab()
46 | with gr.Tab("登录管理"):
47 | login_tab()
48 | with gr.Tab("常见问题"):
49 | problems_tab()
50 |
51 | print("CPP账号的登录是在此控制台,请留意提示!!")
52 | print("点击下面的网址运行程序 ↓↓↓↓↓↓↓↓↓↓↓↓↓↓")
53 | demo.launch(share=args.share, inbrowser=True)
54 |
--------------------------------------------------------------------------------
/main.spec:
--------------------------------------------------------------------------------
1 | # -*- mode: python ; coding: utf-8 -*-
2 | from PyInstaller.utils.hooks import collect_data_files
3 |
4 | datas = []
5 | datas += collect_data_files('gradio_client')
6 | datas += collect_data_files('gradio')
7 |
8 |
9 | a = Analysis(
10 | ['main.py'],
11 | pathex=[],
12 | binaries=[],
13 | datas=datas,
14 | module_collection_mode={
15 | 'gradio': 'py'
16 | },
17 |
18 | hookspath=[],
19 | hooksconfig={},
20 | runtime_hooks=[],
21 | excludes=[],
22 | noarchive=False,
23 | )
24 | pyz = PYZ(a.pure)
25 |
26 | exe = EXE(
27 | pyz,
28 | a.scripts,
29 | a.binaries,
30 | a.datas,
31 | [],
32 | name='cppTickerBuy',
33 | debug=False,
34 | bootloader_ignore_signals=False,
35 | strip=False,
36 | upx=True,
37 | upx_exclude=[],
38 | runtime_tmpdir=None,
39 | console=True,
40 | disable_windowed_traceback=False,
41 | argv_emulation=False,
42 | target_arch=None,
43 | codesign_identity=None,
44 | entitlements_file=None,
45 | icon=['icon.ico']
46 | )
47 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | requests~=2.32.3
2 | setuptools~=65.5.1
3 | gradio~=4.44.1
4 | qrcode~=7.4.2
5 | loguru~=0.7.2
6 | Pillow~=10.3.0
7 | retry~=0.9.2
8 | tinydb~=4.8.0
9 | ntplib~=0.4.0
10 | playsound~=1.3.0
11 | retrying~=1.3.4
12 | pydantic==2.10.6
--------------------------------------------------------------------------------
/solver/aliSolver.py:
--------------------------------------------------------------------------------
1 | import json
2 | import os
3 | import random
4 | import time
5 |
6 | from loguru import logger
7 | from playwright.sync_api import sync_playwright
8 | from retrying import retry
9 |
10 |
11 | def Timer(f):
12 | def inner(*arg, **kwarg):
13 | s_time = time.time()
14 | res = f(*arg, **kwarg)
15 | e_time = time.time()
16 | logger.info('cost:{},res: {}'.format(e_time - s_time, res))
17 | return res
18 |
19 | return inner
20 |
21 |
22 | @Timer
23 | def solve(_browser):
24 | page = _browser.new_page()
25 | page.goto(f"file:///{os.path.abspath('tmp.html')}")
26 | slide_btn = page.query_selector(".btn_slide")
27 | slide_scale = page.query_selector(".nc_scale")
28 | btn_box = slide_btn.bounding_box()
29 | width = slide_scale.bounding_box()['width'] + btn_box['width']
30 | offsets = []
31 | while width > 0:
32 | offset = min(random.randint(50, 70), width)
33 | width -= offset
34 | offsets.append(offset)
35 |
36 | x = btn_box['x'] + btn_box['width'] / 2
37 | y = btn_box['y'] + btn_box['height'] / 2
38 | page.mouse.move(x, y)
39 | page.mouse.down()
40 | for offset in offsets:
41 | page.mouse.move(x, y)
42 | x += offset
43 | page.mouse.up()
44 |
45 | @retry()
46 | def get_result():
47 | return page.query_selector("#mync").inner_html()
48 |
49 | ret = json.loads(get_result())
50 | page.close()
51 | return ret
52 |
53 |
54 | @Timer
55 | def get_edge_browser():
56 | playwright = sync_playwright().start()
57 | browser = playwright.chromium.launch(headless=True, channel="msedge")
58 | context = browser.new_context(
59 | user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) "
60 | "Chrome/91.0.4472.124 Safari/537.36"
61 | )
62 | context.add_init_script(script='Object.defineProperty(navigator, "webdriver", {get: () => undefined})')
63 |
64 | return context
65 |
66 |
67 | if __name__ == '__main__':
68 | br = get_edge_browser()
69 | res = solve(br)
70 | import requests
71 |
72 | url = f"https://www.allcpp.cn/allcpp/ticket/afs/valid.do?sessionId={res['sessionId']}&sig={res['sig']}&outToken={res['token']}"
73 | headers = {
74 | 'accept': 'application/json, text/plain, */*',
75 | 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6,zh-TW;q=0.5,ja;q=0.4',
76 | 'content-type': 'application/json;charset=UTF-8',
77 | 'cookie': '',
78 | 'origin': 'https://cp.allcpp.cn',
79 | 'priority': 'u=1, i',
80 | 'referer': 'https://cp.allcpp.cn/',
81 | 'sec-ch-ua': '"Chromium";v="128", "Not;A=Brand";v="24", "Microsoft Edge";v="128"',
82 | 'sec-ch-ua-mobile': '?0',
83 | 'sec-ch-ua-platform': '"Windows"',
84 | 'sec-fetch-dest': 'empty',
85 | 'sec-fetch-mode': 'cors',
86 | 'sec-fetch-site': 'same-site',
87 | 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 Edg/128.0.0.0'
88 | }
89 |
90 | response = requests.request("POST", url, headers=headers)
91 | print(response.text)
92 |
--------------------------------------------------------------------------------
/solver/tmp.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
29 |
30 |
--------------------------------------------------------------------------------
/tab/go.py:
--------------------------------------------------------------------------------
1 | import hashlib
2 | import json
3 | import secrets
4 | import string
5 | import threading
6 | import time
7 | from datetime import datetime
8 | from json import JSONDecodeError
9 | from urllib.parse import quote
10 |
11 | import gradio as gr
12 | import qrcode
13 | import retry
14 | from gradio import SelectData
15 | from loguru import logger
16 | from requests import HTTPError, RequestException
17 |
18 | from config import global_cookieManager, main_request, configDB, time_service
19 | from util import PushPlusUtil
20 | from util import ServerChanUtil
21 | from util.error import ERRNO_DICT, withTimeString
22 |
23 |
24 | def format_dictionary_to_string(data):
25 | formatted_string_parts = []
26 | for key, value in data.items():
27 | if isinstance(value, list) or isinstance(value, dict):
28 | formatted_string_parts.append(
29 | f"{quote(key)}={quote(json.dumps(value, separators=(',', ':'), ensure_ascii=False))}"
30 | )
31 | else:
32 | formatted_string_parts.append(f"{quote(key)}={quote(str(value))}")
33 |
34 | formatted_string = "&".join(formatted_string_parts)
35 | return formatted_string
36 |
37 |
38 | def go_tab():
39 | isRunning = False
40 |
41 | gr.Markdown("""""")
42 | with gr.Column():
43 | gr.Markdown(
44 | """
45 | ### 上传或填入你要抢票票种的配置信息
46 | """
47 | )
48 | with gr.Row(equal_height=True):
49 | upload_ui = gr.Files(label="上传多个配置文件,点击不同的配置文件可快速切换", file_count="multiple")
50 | ticket_ui = gr.TextArea(
51 | label="填入配置",
52 | info="再次填入配置信息 (不同版本的配置文件可能存在差异,升级版本时候不要偷懒,老版本的配置文件在新版本上可能出问题",
53 | interactive=True
54 | )
55 | gr.HTML(
56 | """
57 | """,
58 | label="选择抢票的时间",
59 | )
60 |
61 | def upload(filepath):
62 | try:
63 | with open(filepath[0], 'r', encoding="utf-8") as file:
64 | content = file.read()
65 | return content
66 | except Exception as e:
67 | return str(e)
68 |
69 | def file_select_handler(select_data: SelectData, files):
70 | file_label = files[select_data.index]
71 | try:
72 | with open(file_label, 'r', encoding="utf-8") as file:
73 | content = file.read()
74 | return content
75 | except Exception as e:
76 | return str(e)
77 |
78 | upload_ui.upload(fn=upload, inputs=upload_ui, outputs=ticket_ui)
79 | upload_ui.select(file_select_handler, upload_ui, ticket_ui)
80 |
81 | # 手动设置/更新时间偏差
82 | with gr.Accordion(label='手动设置/更新时间偏差', open=False):
83 | time_diff_ui = gr.Number(label="当前脚本时间偏差 (单位: ms)",
84 | info="你可以在这里手动输入时间偏差, 或点击下面按钮自动更新当前时间偏差。正值将推迟相应时间开始抢票, 负值将提前相应时间开始抢票。",
85 | value=format(time_service.get_timeoffset() * 1000, '.2f'))
86 | refresh_time_ui = gr.Button(value="点击自动更新时间偏差")
87 | refresh_time_ui.click(fn=lambda: format(float(time_service.compute_timeoffset()) * 1000, '.2f'),
88 | inputs=None, outputs=time_diff_ui)
89 | time_diff_ui.change(fn=lambda x: time_service.set_timeoffset(format(float(x) / 1000, '.5f')),
90 | inputs=time_diff_ui, outputs=None)
91 |
92 | with gr.Accordion(label='配置抢票成功声音提醒[可选]', open=False):
93 | with gr.Row():
94 | audio_path_ui = gr.Audio(
95 | label="上传提示声音", type="filepath", loop=True)
96 |
97 | def input_phone(_phone):
98 | global_cookieManager.set_config_value("phone", _phone)
99 |
100 | with gr.Row():
101 |
102 | interval_ui = gr.Number(
103 | label="抢票间隔",
104 | value=300,
105 | minimum=1,
106 | info="设置抢票任务之间的时间间隔(单位:毫秒),建议不要设置太小",
107 | )
108 | mode_ui = gr.Radio(
109 | label="抢票模式",
110 | choices=["无限", "有限"],
111 | value="无限",
112 | info="选择抢票的模式",
113 | type="index",
114 | interactive=True,
115 | )
116 | total_attempts_ui = gr.Number(
117 | label="总过次数",
118 | value=100,
119 | minimum=1,
120 | info="设置抢票的总次数",
121 | visible=False,
122 | )
123 |
124 | validate_con = threading.Condition()
125 |
126 | def start_go(tickets_info_str, time_start, interval, mode,
127 | total_attempts, audio_path):
128 | nonlocal isRunning
129 | isRunning = True
130 | left_time = total_attempts
131 | yield [
132 | gr.update(value=withTimeString("详细信息见控制台"), visible=True),
133 | gr.update(visible=True),
134 | gr.update(),
135 | gr.update(),
136 |
137 | ]
138 | while isRunning:
139 | try:
140 | if time_start != "":
141 | logger.info("0) 等待开始时间")
142 | timeoffset = time_service.get_timeoffset()
143 | logger.info("时间偏差已被设置为: " + str(timeoffset) + 's')
144 | while isRunning:
145 | try:
146 | time_difference = (
147 | datetime.strptime(time_start, "%Y-%m-%dT%H:%M:%S").timestamp()
148 | - time.time() + timeoffset
149 | )
150 | except ValueError as e:
151 | time_difference = (
152 | datetime.strptime(time_start, "%Y-%m-%dT%H:%M").timestamp()
153 | - time.time() + timeoffset
154 | )
155 | if time_difference > 0:
156 | if time_difference > 5:
157 | yield [
158 | gr.update(value="等待中,剩余等待时间: " + (str(int(
159 | time_difference)) + '秒') if time_difference > 6 else '即将开抢',
160 | visible=True),
161 | gr.update(visible=True),
162 | gr.update(),
163 | gr.update(),
164 |
165 | ]
166 | time.sleep(1)
167 | else:
168 | # 准备倒计时开票, 不再渲染页面, 确保计时准确
169 | # 使用 time.perf_counter() 方法实现高精度计时, 但可能会占用一定的CPU资源
170 | start_time = time.perf_counter()
171 | end_time = start_time + time_difference
172 | current_time = start_time
173 | while current_time < end_time:
174 | current_time = time.perf_counter()
175 | break
176 | if not isRunning:
177 | # 停止定时抢票
178 | yield [
179 | gr.update(value='手动停止定时抢票', visible=True),
180 | gr.update(visible=True),
181 | gr.update(),
182 | gr.update(),
183 | ]
184 | logger.info("手动停止定时抢票")
185 | return
186 | else:
187 | break
188 | # 数据准备
189 | tickets_info = json.loads(tickets_info_str)
190 | people_cur = tickets_info["people_cur"]
191 | ticket_id = tickets_info["tickets"]
192 | _request = main_request
193 | # 订单准备
194 | logger.info(f"1)发起抢票请求")
195 |
196 | @retry.retry(exceptions=RequestException, tries=60, delay=interval / 1000)
197 | def inner_request():
198 | nonlocal isRunning
199 | if not isRunning:
200 | raise ValueError("抢票结束")
201 |
202 | timestamp = int(time.time())
203 | n = string.ascii_letters + string.digits
204 | nonce = ''.join(secrets.choice(n) for i in range(32))
205 | sign = hashlib.md5(f"2x052A0A1u222{timestamp}{nonce}{ticket_id}2sFRs".encode('utf-8')).hexdigest()
206 |
207 | ret = _request.post(
208 | url=f"https://www.allcpp.cn/allcpp/ticket/buyTicketWeixin.do?ticketTypeId={ticket_id}"
209 | f"&count={len(people_cur)}&nonce={nonce}&timeStamp={timestamp}&sign={sign}&payType=0&"
210 | f"purchaserIds={','.join([str(p['id']) for p in people_cur])}",
211 | ).json()
212 | err = ret["isSuccess"]
213 | logger.info(
214 | f'状态码: {err}({ERRNO_DICT.get(err, "未知错误码")}), 请求体: {ret}'
215 | )
216 | if ret["message"] == "同证件限购一张!":
217 | isRunning = False
218 | raise ValueError("同证件限购一张!")
219 |
220 | if ret["message"] == "请求过于频繁,请稍后再试":
221 | logger.info(
222 | "出现风控,重新登录"
223 | )
224 | # _request.refreshToken()
225 |
226 | if not ret["isSuccess"]:
227 | raise HTTPError("重试次数过多,重新准备订单")
228 | return ret, err
229 |
230 | request_result, errno = inner_request()
231 | left_time_str = "无限" if mode == 0 else left_time
232 | logger.info(
233 | f'状态码: {errno}({ERRNO_DICT.get(errno, "未知错误码")}), 请求体: {request_result} 剩余次数: {left_time_str}'
234 | )
235 | yield [
236 | gr.update(
237 | value=withTimeString(
238 | f"正在抢票,具体情况查看终端控制台。\n剩余次数: {left_time_str}\n当前状态码: {errno} ({ERRNO_DICT.get(errno, '未知错误码')})"),
239 | visible=True,
240 | ),
241 | gr.update(visible=True),
242 | gr.update(),
243 | gr.update(),
244 | ]
245 | if errno:
246 | logger.info(f"2)微信扫码支付(暂不支持支付宝)")
247 | qr_gen = qrcode.QRCode()
248 | qr_gen.add_data(request_result['result']['code'])
249 | qr_gen.make(fit=True)
250 | qr_gen_image = qr_gen.make_image()
251 | yield [
252 | gr.update(value=withTimeString("生成付款二维码"), visible=True),
253 | gr.update(visible=False),
254 | gr.update(value=qr_gen_image.get_image(), visible=True),
255 | gr.update(),
256 |
257 | ]
258 | pushplusToken = configDB.get("pushplusToken")
259 | if pushplusToken is not None and pushplusToken != "":
260 | PushPlusUtil.send_message(pushplusToken, "抢票成功", "付款吧")
261 | serverchanKey = configDB.get("serverchanKey")
262 | if serverchanKey is not None and serverchanKey != "":
263 | ServerChanUtil.send_message(serverchanKey, "抢票成功", "付款吧")
264 |
265 | if audio_path is not None and audio_path != "":
266 | yield [
267 | gr.update(value="开始放歌, 暂未实现关闭音乐功能,想关闭音乐请重启程序", visible=True),
268 | gr.update(visible=False),
269 | gr.update(),
270 | gr.update(value=audio_path, autoplay=True),
271 | ]
272 |
273 | break
274 | if mode == 1:
275 | left_time -= 1
276 | if left_time <= 0:
277 | break
278 | except JSONDecodeError as e:
279 | logger.error(f"配置文件格式错误: {e}")
280 | return [
281 | gr.update(value=withTimeString("配置文件格式错误"), visible=True),
282 | gr.update(visible=True),
283 | gr.update(),
284 | gr.update(),
285 |
286 | ]
287 | except ValueError as e:
288 | logger.info(f"{e}")
289 | yield [
290 | gr.update(value=withTimeString(f"有错误,具体查看控制台日志\n\n当前错误 {e}"), visible=True),
291 | gr.update(visible=True),
292 | gr.update(),
293 | gr.update(),
294 |
295 | ]
296 | except HTTPError as e:
297 | logger.error(f"请求错误: {e}")
298 | yield [
299 | gr.update(value=withTimeString(f"有错误,具体查看控制台日志\n\n当前错误 {e}"), visible=True),
300 | gr.update(visible=True),
301 | gr.update(),
302 | gr.update(),
303 |
304 | ]
305 | except Exception as e:
306 | logger.exception(e)
307 | yield [
308 | gr.update(value=withTimeString(f"有错误,具体查看控制台日志\n\n当前错误 {e}"), visible=True),
309 | gr.update(visible=True),
310 | gr.update(),
311 | gr.update(),
312 |
313 | ]
314 | finally:
315 | time.sleep(interval / 1000.0)
316 |
317 | yield [
318 | gr.update(value="抢票结束,具体查看控制台日志", visible=True),
319 | gr.update(visible=False), # 当设置play_sound_process,应该有提示声音
320 | gr.update(),
321 | gr.update(),
322 | ]
323 |
324 | mode_ui.change(
325 | fn=lambda x: gr.update(visible=True)
326 | if x == 1
327 | else gr.update(visible=False),
328 | inputs=[mode_ui],
329 | outputs=total_attempts_ui,
330 | )
331 | with gr.Row():
332 | go_btn = gr.Button("开始抢票")
333 | stop_btn = gr.Button("停止", visible=False)
334 |
335 | with gr.Row():
336 | go_ui = gr.Textbox(
337 | info="此窗口为临时输出,具体请见控制台",
338 | label="输出信息",
339 | interactive=False,
340 | visible=False,
341 | show_copy_button=True,
342 | max_lines=10,
343 |
344 | )
345 | qr_image = gr.Image(label="使用微信扫码支付", visible=False, elem_classes="pay_qrcode")
346 |
347 | time_tmp = gr.Textbox(visible=False)
348 |
349 | go_btn.click(
350 | fn=None,
351 | inputs=None,
352 | outputs=time_tmp,
353 | js='(x) => document.getElementById("datetime").value',
354 | )
355 |
356 | def stop():
357 | nonlocal isRunning
358 | isRunning = False
359 |
360 | go_btn.click(
361 | fn=start_go,
362 | inputs=[ticket_ui, time_tmp, interval_ui, mode_ui,
363 | total_attempts_ui, audio_path_ui],
364 | outputs=[go_ui, stop_btn, qr_image, audio_path_ui],
365 | )
366 | stop_btn.click(
367 | fn=stop,
368 | inputs=None,
369 | outputs=None,
370 | )
371 |
--------------------------------------------------------------------------------
/tab/login.py:
--------------------------------------------------------------------------------
1 | import gradio as gr
2 | from loguru import logger
3 |
4 | from config import main_request, configDB, global_cookieManager
5 | from util.KVDatabase import KVDatabase
6 |
7 | names = []
8 |
9 |
10 | @logger.catch
11 | def login_tab():
12 | gr.Markdown("""
13 | > **补充**
14 | >
15 | > 在这里,你可以
16 | > 1. 去更改账号,
17 | > 2. 查看当前程序正在使用哪个账号
18 | > 3. 使用配置文件切换到另一个账号
19 | >
20 | """)
21 | with gr.Row():
22 | username_ui = gr.Text(
23 | main_request.get_request_name(),
24 | label="账号名称",
25 | interactive=False,
26 | info="当前账号的名称",
27 | )
28 | gr_file_ui = gr.File(label="当前登录信息文件",
29 | value=configDB.get("cookie_path"))
30 | gr.Markdown("""🏵️ 登录
31 |
32 | > 请不要一个程序打开多次
33 | > 如果这些程序都是同一个文件打开的,当你修改其中这个程序的账号时候,也会影响其他程序""")
34 | info_ui = gr.TextArea(
35 | info="此窗口为输出信息", label="输出信息", interactive=False
36 | )
37 | with gr.Row():
38 | upload_ui = gr.UploadButton(label="导入")
39 | add_btn = gr.Button("登录")
40 |
41 | def upload_file(filepath):
42 | main_request.cookieManager.db.delete("cookie")
43 | yield ["已经注销,请选择登录信息文件", gr.update(), gr.update()]
44 | try:
45 | configDB.insert("cookie_path", filepath)
46 | global_cookieManager.db = KVDatabase(filepath)
47 | name = main_request.get_request_name()
48 | yield [gr.update(value="导入成功"), gr.update(value=name), gr.update(value=configDB.get("cookie_path"))]
49 | except Exception:
50 | name = main_request.get_request_name()
51 | yield ["登录出现错误", gr.update(value=name), gr.update(value=configDB.get("cookie_path"))]
52 |
53 | upload_ui.upload(upload_file, [upload_ui], [info_ui, username_ui, gr_file_ui])
54 |
55 | def add():
56 | main_request.cookieManager.db.delete("cookie")
57 | yield ["已经注销,在控制台(终端)登录", gr.update(value="未登录"),
58 | gr.update(value=configDB.get("cookie_path"))]
59 | try:
60 | main_request.cookieManager.get_cookies_str_force()
61 | name = main_request.get_request_name()
62 | yield [f"登录成功", gr.update(value=name), gr.update(value=configDB.get("cookie_path"))]
63 | except Exception:
64 | name = main_request.get_request_name()
65 | yield ["登录出现错误", gr.update(value=name), gr.update(value=configDB.get("cookie_path"))]
66 |
67 | add_btn.click(
68 | fn=add,
69 | inputs=None,
70 | outputs=[info_ui, username_ui, gr_file_ui]
71 | )
72 | gr.Markdown(
73 | """
74 | 🗨️ 抢票成功提醒
75 | > 你需要去对应的网站获取key或token,然后填入下面的输入框
76 | > [Server酱](https://sct.ftqq.com/sendkey) | [pushplus](https://www.pushplus.plus/uc.html)
77 | > 留空以不启用提醒功能
78 | """)
79 | with gr.Row():
80 | serverchan_ui = gr.Textbox(
81 | value=configDB.get("serverchanKey") if configDB.get("serverchanKey") is not None else "",
82 | label="Server酱的SendKey",
83 | interactive=True,
84 | info="https://sct.ftqq.com/",
85 | )
86 |
87 | pushplus_ui = gr.Textbox(
88 | value=configDB.get("pushplusToken") if configDB.get("pushplusToken") is not None else "",
89 | label="PushPlus的Token",
90 | interactive=True,
91 | info="https://www.pushplus.plus/",
92 | )
93 |
94 | def inner_input_serverchan(x):
95 | return configDB.insert("serverchanKey", x)
96 | def inner_input_pushplus(x):
97 | return configDB.insert("pushplusToken", x)
98 |
99 | serverchan_ui.change(fn=inner_input_serverchan, inputs=serverchan_ui)
100 |
101 | pushplus_ui.change(fn=inner_input_pushplus, inputs=pushplus_ui)
102 |
--------------------------------------------------------------------------------
/tab/order.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 |
3 | import gradio as gr
4 | import loguru
5 | import pandas
6 | import qrcode
7 |
8 | from config import main_request
9 |
10 |
11 | def order_tab():
12 | # Function to fetch data from API
13 | orders = []
14 | orders_dict = []
15 | orders_str = []
16 |
17 | def get_order_list():
18 | nonlocal orders, orders_dict, orders_str
19 | try:
20 | resp = main_request.get(url="https://www.allcpp.cn/api/tk/getList.do?type=0&sort=0&index=1&size=100").json()
21 | orders = resp["result"]["data"]
22 | loguru.logger.info(f"获取订单: {orders}")
23 | orders_dict = [
24 | {
25 | "id": order["id"],
26 | "eventName": order["eventName"],
27 | "ticketName": order["ticketName"],
28 | "createTime": datetime.utcfromtimestamp(order["createTime"] / 1000).strftime('%Y-%m-%d %H:%M:%S')
29 | } for order in orders
30 | ]
31 | orders_str = [
32 | f'{order["eventName"]}- {order["ticketName"]}-{order["createTime"]}'
33 | for order in orders]
34 | df = pandas.DataFrame(orders)
35 | return [gr.update(value=df), gr.update(choices=orders_str)]
36 | except Exception as e:
37 | loguru.logger.exception(e)
38 | return [gr.update(), gr.update()]
39 |
40 | # Create Gradio app
41 | order_list_ui = gr.Dataframe(value=[], row_count=(2, "dynamic"))
42 | load_btn_ui = gr.Button("加载订单")
43 |
44 | with gr.Blocks() as order_pay:
45 | order_pay_ui = gr.Dropdown(label="选择付款的订单", interactive=True, type="index")
46 | buy_btn_ui = gr.Button("购买该票")
47 | log_ui = gr.JSON(label="打印日志")
48 | qr_image = gr.Image(label="使用微信扫码支付", elem_classes="pay_qrcode")
49 |
50 | def buy_order(order_idx):
51 | order = orders[order_idx]
52 | url = f"https://www.allcpp.cn/allcpp/ticket/buyTicketForOrder.do?orderid={order['id']}&ticketInfo=undefined,{order['ticketCount']},{order['price']}&paytype=1 "
53 | resp = main_request.post(url=url).json()
54 | loguru.logger.info(f"支付订单: {resp}")
55 | qr_gen = qrcode.QRCode()
56 | qr_gen.add_data(resp['result']['code'])
57 | qr_gen.make(fit=True)
58 | qr_gen_image = qr_gen.make_image()
59 | return [gr.update(value=resp), gr.update(value=qr_gen_image.get_image())]
60 |
61 | buy_btn_ui.click(fn=buy_order, inputs=order_pay_ui, outputs=[log_ui, qr_image])
62 | load_btn_ui.click(fn=get_order_list, inputs=None, outputs=[order_list_ui, order_pay_ui])
63 |
--------------------------------------------------------------------------------
/tab/problems.py:
--------------------------------------------------------------------------------
1 | import gradio as gr
2 |
3 |
4 | def problems_tab():
5 | gr.Markdown("""
6 | 本项目的提问专区 [提问专区](https://github.com/mikumifa/cppTickerBuy/discussions)
7 | """)
8 |
--------------------------------------------------------------------------------
/tab/settings.py:
--------------------------------------------------------------------------------
1 | import json
2 | import os
3 | import re
4 | from datetime import datetime
5 | from urllib.parse import urlparse, parse_qs
6 |
7 | import gradio as gr
8 | from loguru import logger
9 |
10 | from config import main_request, TEMP_PATH
11 |
12 | buyer_value = []
13 | addr_value = []
14 | ticket_value = []
15 | project_name = []
16 | ticket_str_list = []
17 |
18 |
19 | def convert_timestamp_to_str(timestamp):
20 | return datetime.fromtimestamp(timestamp / 1000).strftime('%Y-%m-%d %H:%M:%S')
21 |
22 |
23 | def filename_filter(filename):
24 | filename = re.sub('[\/:*?"<>|]', '', filename)
25 | return filename
26 |
27 |
28 | def on_submit_ticket_id(num):
29 | global buyer_value
30 | global addr_value
31 | global ticket_value
32 | global project_name
33 | global ticket_str_list
34 |
35 | try:
36 | buyer_value = []
37 | addr_value = []
38 | ticket_value = []
39 | if "http" in num or "https" in num:
40 | num = extract_id_from_url(num)
41 | extracted_id_message = f"已提取URL票ID:{num}"
42 | else:
43 | return [
44 | gr.update(),
45 | gr.update(),
46 | gr.update(visible=False),
47 | gr.update(value='输入无效,请输入一个有效的网址。', visible=True),
48 | ]
49 | ret = main_request.get(
50 | url=f"https://www.allcpp.cn/allcpp/ticket/getTicketTypeList.do?eventMainId={num}"
51 | )
52 | ret = ret.json()
53 | logger.debug(ret)
54 |
55 | # 检查 errno
56 | if "ticketMain" not in ret:
57 | return [
58 | gr.update(),
59 | gr.update(),
60 | gr.update(visible=True),
61 | gr.update(value='输入无效,请输入一个有效的票。', visible=True),
62 | ]
63 |
64 | ticketMain = ret['ticketMain']
65 | ticketTypeList = ret["ticketTypeList"]
66 | project_name = ticketMain['eventName']
67 | ticket_str_list = [
68 | (
69 | f"{ticket['square']}-{ticket['ticketName']} - 开始时间: {convert_timestamp_to_str(ticket['sellStartTime'])} - 截止时间: "
70 | f"{convert_timestamp_to_str(ticket['sellEndTime'])}- 描述: {ticket['ticketDescription']}")
71 | for ticket in ticketTypeList
72 | ]
73 | ticket_value = [
74 | ticket['id']
75 | for ticket in ticketTypeList
76 | ]
77 | buyer_value = main_request.get(
78 | url=f"https://www.allcpp.cn/allcpp/user/purchaser/getList.do"
79 | ).json()
80 | logger.debug(buyer_value)
81 | buyer_str_list = [
82 | f"{item['realname']}-{item['idcard']}-{item['mobile']}" for item in buyer_value
83 | ]
84 |
85 | return [
86 | gr.update(choices=ticket_str_list),
87 | gr.update(choices=buyer_str_list),
88 | gr.update(visible=True),
89 | gr.update(
90 | value=f"{extracted_id_message}\n"
91 | f"获取票信息成功:\n"
92 | f"活动名称:{ticketMain['eventName']}\n\n"
93 | f"{ticketMain['description']}\n"
94 | f"{ticketMain['eventDescription']}\n",
95 | visible=True,
96 | ),
97 | ]
98 | except Exception as e:
99 | return [
100 | gr.update(),
101 | gr.update(),
102 | gr.update(),
103 | gr.update(value=f"发生错误:{e}", visible=True),
104 | ]
105 |
106 |
107 | def extract_id_from_url(url):
108 | parsed_url = urlparse(url)
109 | query_params = parse_qs(parsed_url.query)
110 | return query_params.get('event', [None])[0]
111 |
112 |
113 | def on_submit_all(ticket_id, ticket_info, people_indices):
114 | try:
115 | # if ticket_number != len(people_indices):
116 | # return gr.update(
117 | # value="生成配置文件失败,保证选票数目和购买人数目一致", visible=True
118 | # )
119 | ticket_cur = ticket_value[ticket_info]
120 | people_cur = [buyer_value[item] for item in people_indices]
121 | ticket_id = extract_id_from_url(ticket_id)
122 | if ticket_id is None:
123 | return [gr.update(value="你所填不是网址,或者网址是错的", visible=True),
124 | gr.update(value={}),
125 | gr.update()]
126 | if len(people_indices) == 0:
127 | return [gr.update(value="至少选一个实名人", visible=True),
128 | gr.update(value={}),
129 | gr.update()]
130 | detail = f'{project_name}-{ticket_str_list[ticket_info]}'
131 | config_dir = {
132 | 'detail': detail,
133 | 'tickets': ticket_cur,
134 | 'people_cur': people_cur
135 | }
136 | filename = os.path.join(TEMP_PATH, filename_filter(detail) + ".json")
137 | with open(filename, 'w', encoding='utf-8') as f:
138 | json.dump(config_dir, f, ensure_ascii=False, indent=4)
139 | return [gr.update(), gr.update(value=config_dir, visible=True), gr.update(value=filename, visible=True)]
140 | except Exception as e:
141 | return [gr.update(value="生成错误,仔细看看你可能有哪里漏填的", visible=True), gr.update(value={}),
142 | gr.update()]
143 |
144 |
145 | def setting_tab():
146 | gr.Markdown("""
147 | > **必看**
148 | >
149 | > 保证自己在抢票前,已经配置了购买人信息(就算不需要也要提前填写) 如果没填,生成表单时候不会出现任何选项
150 | > - 购买人信息:https://cp.allcpp.cn/ticket/prePurchaser
151 | """)
152 | info_ui = gr.TextArea(
153 | info="此窗口为输出信息", label="输出信息", interactive=False, visible=False
154 | )
155 | with gr.Column():
156 | ticket_id_ui = gr.Textbox(
157 | label="想要抢票的网址",
158 | interactive=True,
159 | info="例如:https://www.allcpp.cn/allcpp/event/event.do?event=3163",
160 | )
161 | ticket_id_btn = gr.Button("获取票信息")
162 | with gr.Column(visible=False) as inner:
163 | with gr.Row():
164 | people_ui = gr.CheckboxGroup(
165 | label="身份证实名认证",
166 | interactive=True,
167 | type="index",
168 | info="必填,选几个就代表买几个人的票,在 https://cp.allcpp.cn/ticket/prePurchaser 中添加",
169 | )
170 | ticket_info_ui = gr.Dropdown(
171 | label="选票",
172 | interactive=True,
173 | type="index",
174 | info="必填,请仔细核对起售时间,千万别选错其他时间点的票",
175 | )
176 |
177 | config_btn = gr.Button("生成配置")
178 | config_file_ui = gr.File(visible=False)
179 | config_output_ui = gr.JSON(
180 | label="生成配置文件(右上角复制)",
181 | visible=False,
182 | )
183 | config_btn.click(
184 | fn=on_submit_all,
185 | inputs=[
186 | ticket_id_ui,
187 | ticket_info_ui,
188 | people_ui
189 | ],
190 | outputs=[info_ui, config_output_ui, config_file_ui]
191 | )
192 |
193 | ticket_id_btn.click(
194 | fn=on_submit_ticket_id,
195 | inputs=ticket_id_ui,
196 | outputs=[
197 | ticket_info_ui,
198 | people_ui,
199 | inner,
200 | info_ui,
201 | ],
202 | )
203 |
--------------------------------------------------------------------------------
/util/CookieManager.py:
--------------------------------------------------------------------------------
1 | import requests
2 | from loguru import logger
3 |
4 | from util.KVDatabase import KVDatabase
5 |
6 |
7 | class CookieManager:
8 | def __init__(self, config_file_path):
9 | self.db = KVDatabase(config_file_path)
10 |
11 | @logger.catch
12 | def _login_and_save_cookies(
13 | self, login_url="https://cp.allcpp.cn/#/login/main"
14 | ):
15 | print("开始填写登录信息")
16 | phone = input("输入手机号:")
17 | password = input("输入密码:")
18 | login_url = "https://user.allcpp.cn/api/login/normal"
19 | headers = {
20 | 'accept': 'application/json, text/plain, */*',
21 | 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6,zh-TW;q=0.5,ja;q=0.4',
22 | 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8',
23 | 'origin': 'https://cp.allcpp.cn',
24 | 'priority': 'u=1, i',
25 | 'referer': 'https://cp.allcpp.cn/',
26 | 'sec-ch-ua': '"Not)A;Brand";v="99", "Microsoft Edge";v="127", "Chromium";v="127"',
27 | 'sec-ch-ua-mobile': '?0',
28 | 'sec-ch-ua-platform': '"Windows"',
29 | 'sec-fetch-dest': 'empty',
30 | 'sec-fetch-mode': 'cors',
31 | 'sec-fetch-site': 'same-site',
32 | 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 Edg/127.0.0.0'
33 | }
34 | while True:
35 | payload = f"account={phone}&password={password}&phoneAccountBindToken=undefined&thirdAccountBindToken=undefined"
36 | response = requests.request("POST", login_url, headers=headers, data=payload)
37 | res_json = response.json()
38 | logger.info(f"登录响应体: {res_json}")
39 | if "token" in res_json:
40 | cookies_dict = response.cookies.get_dict()
41 | logger.info(f"cookies: {cookies_dict}")
42 | self.db.insert("cookie", cookies_dict)
43 | self.db.insert("password", password)
44 | self.db.insert("phone", phone)
45 |
46 | return response.cookies
47 | else:
48 | phone = input("输入手机号:")
49 | password = input("输入密码:")
50 |
51 | def refreshToken(self):
52 | login_url = "https://user.allcpp.cn/api/login/normal"
53 | headers = {
54 | 'accept': 'application/json, text/plain, */*',
55 | 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6,zh-TW;q=0.5,ja;q=0.4',
56 | 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8',
57 | 'origin': 'https://cp.allcpp.cn',
58 | 'priority': 'u=1, i',
59 | 'referer': 'https://cp.allcpp.cn/',
60 | 'sec-ch-ua': '"Not)A;Brand";v="99", "Microsoft Edge";v="127", "Chromium";v="127"',
61 | 'sec-ch-ua-mobile': '?0',
62 | 'sec-ch-ua-platform': '"Windows"',
63 | 'sec-fetch-dest': 'empty',
64 | 'sec-fetch-mode': 'cors',
65 | 'sec-fetch-site': 'same-site',
66 | 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 Edg/127.0.0.0'
67 | }
68 | phone = self.db.get("phone")
69 | password = self.db.get("password")
70 | payload = f"account={phone}&password={password}&phoneAccountBindToken=undefined&thirdAccountBindToken=undefined"
71 | response = requests.request("POST", login_url, headers=headers, data=payload)
72 | res_json = response.json()
73 | logger.info(f"刷新登录响应体: {res_json}")
74 | if "token" in res_json:
75 | cookies_dict = response.cookies.get_dict()
76 | logger.info(f"cookies: {cookies_dict}")
77 | self.db.insert("cookie", cookies_dict)
78 | return cookies_dict
79 |
80 | def get_cookies(self, force=False):
81 | if force:
82 | return self.db.get("cookie")
83 | if not self.db.contains("cookie") or not self.db.contains("password") or not self.db.contains("phone"):
84 | return self._login_and_save_cookies()
85 | else:
86 | return self.db.get("cookie")
87 |
88 | def have_cookies(self):
89 | return self.db.contains("cookie") and self.db.contains("password") and self.db.contains("phone")
90 |
91 | def get_cookies_str(self):
92 | cookies = self.get_cookies()
93 | cookies_str = ""
94 | for key in cookies.keys():
95 | cookies_str += key + "=" + cookies[key] + "; "
96 | return cookies_str
97 |
98 | def get_cookies_value(self, name):
99 | cookies = self.get_cookies()
100 | for cookie in cookies:
101 | if cookie["name"] == name:
102 | return cookie["value"]
103 | return None
104 |
105 | def get_config_value(self, name, default=None):
106 | if self.db.contains(name):
107 | return self.db.get(name)
108 | else:
109 | return default
110 |
111 | def set_config_value(self, name, value):
112 | self.db.insert(name, value)
113 |
114 | def get_cookies_str_force(self):
115 | self._login_and_save_cookies()
116 | return self.get_cookies_str()
117 |
--------------------------------------------------------------------------------
/util/CppRequest.py:
--------------------------------------------------------------------------------
1 | import requests
2 |
3 | from util.CookieManager import CookieManager
4 |
5 |
6 | class CppRequest:
7 | def __init__(self, headers=None, cookies_config_path=""):
8 | self.session = requests.Session()
9 | self.cookieManager = CookieManager(cookies_config_path)
10 | self.headers = headers or {
11 | 'accept': 'application/json, text/plain, */*',
12 | 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6,zh-TW;q=0.5,ja;q=0.4',
13 | 'cookie': "",
14 | 'origin': 'https://cp.allcpp.cn',
15 | 'priority': 'u=1, i',
16 | 'referer': 'https://cp.allcpp.cn/',
17 | 'sec-ch-ua': '"Not)A;Brand";v="99", "Microsoft Edge";v="127", "Chromium";v="127"',
18 | 'sec-ch-ua-mobile': '?0',
19 | 'sec-ch-ua-platform': '"Windows"',
20 | 'sec-fetch-dest': 'empty',
21 | 'sec-fetch-mode': 'cors',
22 | 'sec-fetch-site': 'same-site',
23 | 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 Edg/127.0.0.0'
24 | }
25 |
26 | def get(self, url, data=None):
27 | self.headers["cookie"] = self.cookieManager.get_cookies_str()
28 | response = self.session.get(url, data=data, headers=self.headers)
29 | response.raise_for_status()
30 | return response
31 |
32 | def post(self, url, data=None):
33 | self.headers["cookie"] = self.cookieManager.get_cookies_str()
34 | response = self.session.post(url, data=data, headers=self.headers)
35 | response.raise_for_status()
36 | return response
37 |
38 | def get_request_name(self):
39 | try:
40 | if not self.cookieManager.have_cookies():
41 | return "未登录"
42 | result = self.get("https://www.allcpp.cn/allcpp/circle/getCircleMannage.do").json()
43 | return result["result"]["joinCircleList"][0]["nickname"]
44 | except Exception as e:
45 | return "未登录"
46 |
47 | def refreshToken(self):
48 | self.cookieManager.refreshToken()
49 |
50 |
51 | if __name__ == "__main__":
52 | test_request = CppRequest(cookies_config_path="cookies.json")
53 | res = test_request.get("https://www.allcpp.cn/api/tk/getList.do?type=1&sort=0&index=1&size=10")
54 | print(res.headers)
55 | print(res.text)
56 |
--------------------------------------------------------------------------------
/util/KVDatabase.py:
--------------------------------------------------------------------------------
1 | from tinydb import TinyDB, Query
2 |
3 |
4 | class KVDatabase:
5 | def __init__(self, db_path='kv_db.json'):
6 | self.db = TinyDB(db_path)
7 | self.KeyValue = Query()
8 |
9 |
10 | def insert(self, key, value):
11 | # 如果键已经存在,更新其值;否则插入新键值对
12 | if self.db.contains(self.KeyValue.key == key):
13 | self.db.update({'value': value}, self.KeyValue.key == key)
14 | else:
15 | self.db.insert({'key': key, 'value': value})
16 |
17 | def get(self, key):
18 | result = self.db.get(self.KeyValue.key == key)
19 | return result['value'] if result else None
20 |
21 | def update(self, key, value):
22 | if self.db.contains(self.KeyValue.key == key):
23 | self.db.update({'value': value}, self.KeyValue.key == key)
24 | else:
25 | raise KeyError(f"Key '{key}' not found in database.")
26 |
27 | def delete(self, key):
28 | self.db.remove(self.KeyValue.key == key)
29 |
30 | def contains(self, key):
31 | return self.db.contains(self.KeyValue.key == key)
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/util/PushPlusUtil.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | import loguru
4 | import requests
5 | import playsound
6 | import os
7 |
8 |
9 |
10 | def send_message(token, content, title):
11 | try:
12 | url = "http://www.pushplus.plus/send"
13 | headers = {
14 | "Content-Type": "application/json"
15 | }
16 |
17 | data = {
18 | "token": token,
19 | "content": content,
20 | "title": title
21 | }
22 | requests.post(url, headers=headers, data=json.dumps(data))
23 | except Exception as e:
24 | loguru.logger.info("PushPlus消息发送失败")
25 |
26 |
27 |
28 |
29 |
30 | if __name__ == '__main__':
31 | playsound.playsound(os.path.join(get_application_tmp_path(), "default.mp3"))
32 |
--------------------------------------------------------------------------------
/util/ServerChanUtil.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | import loguru
4 | import requests
5 | import playsound
6 | import os
7 |
8 |
9 |
10 | def send_message(token, desp, title):
11 | try:
12 | url = f"https://sctapi.ftqq.com/{token}.send"
13 | headers = {
14 | "Content-Type": "application/json"
15 | }
16 |
17 | data = {
18 | "desp": desp,
19 | "title": title
20 | }
21 | requests.post(url, headers=headers, data=json.dumps(data))
22 | except Exception as e:
23 | loguru.logger.info("ServerChan消息发送失败")
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/util/TimeService.py:
--------------------------------------------------------------------------------
1 | import ntplib
2 | import time
3 | from loguru import logger
4 |
5 |
6 | class TimeService:
7 | # NTP服务器默认为ntp.aliyun.com, 可根据实际情况修改
8 | def __init__(self, _ntp_server="ntp.aliyun.com") -> None:
9 | self.ntp_server = _ntp_server
10 | self.client = ntplib.NTPClient()
11 | self.timeoffset: float = 0
12 |
13 | def compute_timeoffset(self) -> str:
14 | """
15 | 返回的timeoffset单位为秒
16 | """
17 | # NTP时间请求有可能会超时失败, 设定三次重试机会
18 | for i in range(0, 3):
19 | try:
20 | response = self.client.request(self.ntp_server, version=4)
21 | break
22 | except Exception as e:
23 | logger.warning("第" + str(i + 1) + "次获取NTP时间失败, 尝试重新获取")
24 | if i == 2:
25 | return "error"
26 | time.sleep(0.5)
27 | logger.info("时间同步成功, 将使用" + self.ntp_server + "时间")
28 | # response.offset 为[NTP时钟源 - 设备时钟]的偏差, 使用时需要取反
29 | return format(-(response.offset), ".5f")
30 |
31 | def set_timeoffset(self, _timeoffset: str) -> None:
32 | """
33 | 传入的timeoffset单位为秒
34 | """
35 | if _timeoffset == "error":
36 | self.timeoffset = 0
37 | logger.warning("NTP时间同步失败, 使用本地时间")
38 | else:
39 | self.timeoffset = float(_timeoffset)
40 | logger.info("设置时间偏差为: " + str(self.timeoffset) + "秒")
41 |
42 | def get_timeoffset(self) -> float:
43 | """
44 | 获取到的timeoffset单位为秒
45 | """
46 | return self.timeoffset
47 |
--------------------------------------------------------------------------------
/util/error.py:
--------------------------------------------------------------------------------
1 | # 欢迎补充错误码
2 | import datetime
3 |
4 | ERRNO_DICT = {
5 | False: '抢票失败'
6 | }
7 |
8 |
9 | def withTimeString(string):
10 | return f"{datetime.datetime.now()}: {string}"
11 |
--------------------------------------------------------------------------------