├── .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 | logo 4 | 5 |

cppTickerBuy

6 | 7 | ![GitHub all releases](https://img.shields.io/github/downloads/mikumifa/cppTickerBuy/total) 8 | ![GitHub release (with filter)](https://img.shields.io/github/v/release/mikumifa/cppTickerBuy) 9 | ![GitHub issues](https://img.shields.io/github/issues/mikumifa/cppTickerBuy) 10 | ![GitHub Repo stars](https://img.shields.io/github/stars/mikumifa/cppTickerBuy) 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 | 61 | 62 | 63 | 70 | 77 | 78 | 79 |
64 | 65 | mikumifa 66 |
67 | mikumifa 68 |
69 |
71 | 72 | WittF 73 |
74 | W1ttF 75 |
76 |
80 | 81 | 82 | 83 | ## Star History 84 | 85 | [![Star History Chart](https://api.star-history.com/svg?repos=mikumifa/cppTickerBuy&type=Date)](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 | --------------------------------------------------------------------------------