├── assest
├── fsm.png
├── alarm.wav
└── icon.ico
├── interface
├── __init__.py
└── CLI
│ ├── __init__.py
│ ├── product.py
│ ├── user.py
│ └── setting.py
├── .editorconfig
├── .github
├── ISSUE_TEMPLATE
│ ├── feature.md
│ └── bug.md
└── workflows
│ └── ci.yml
├── util
├── __init__.py
├── Geetest
│ └── __init__.py
├── Notice
│ └── __init__.py
├── Config
│ └── __init__.py
├── Captcha
│ └── __init__.py
├── Request
│ └── __init__.py
├── Info
│ └── __init__.py
├── Task
│ └── __init__.py
├── Data
│ └── __init__.py
├── Bilibili
│ └── __init__.py
└── Login
│ └── __init__.py
├── .pre-commit-config.yaml
├── flake.nix
├── cli.spec
├── pyproject.toml
├── flake.lock
├── README.md
├── cli.py
├── .gitignore
└── LICENSE
/assest/fsm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ZianTT/bilibili-ticket-python/HEAD/assest/fsm.png
--------------------------------------------------------------------------------
/assest/alarm.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ZianTT/bilibili-ticket-python/HEAD/assest/alarm.wav
--------------------------------------------------------------------------------
/assest/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ZianTT/bilibili-ticket-python/HEAD/assest/icon.ico
--------------------------------------------------------------------------------
/interface/__init__.py:
--------------------------------------------------------------------------------
1 | from interface.CLI import ProductCli, SettingCli, UserCli
2 |
3 | __all__ = ["UserCli", "ProductCli", "SettingCli"]
4 |
--------------------------------------------------------------------------------
/interface/CLI/__init__.py:
--------------------------------------------------------------------------------
1 | from interface.CLI.product import ProductCli
2 | from interface.CLI.setting import SettingCli
3 | from interface.CLI.user import UserCli
4 |
5 | __all__ = ["UserCli", "ProductCli", "SettingCli"]
6 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | insert_final_newline = true
7 | indent_style = space
8 | indent_size = 4
9 | trim_trailing_whitespace = true
10 |
11 | [*.py]
12 | profile = black
13 |
14 | [*.yml]
15 | indent_size = 2
16 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature
3 | about: 项目建议
4 | title: "【建议】"
5 | labels: 增强
6 | assignees: ''
7 |
8 | ---
9 |
10 | **该功能是否和现有BUG有关**
11 | 有/无, 如有那么关联在哪
12 |
13 | **描述需求**
14 | 你期望实现什么样的功能
15 |
16 | **解决思路**
17 | 如有想法可说一说自己的想法建议, 条件允许可以发送PR
18 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug
3 | about: 提交错误内容帮助我们改进
4 | title: "【BUG】"
5 | labels: Bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | **问题情况**
11 | 请说明一下发生了什么
12 |
13 | **日志**
14 | 如果可以的话请截取相关情况的Log
15 |
16 | **操作环境**
17 | - 操作系统: (例如: Windows 11)
18 | - 抢票活动: (例如: Bilibili World 2024)
19 |
--------------------------------------------------------------------------------
/util/__init__.py:
--------------------------------------------------------------------------------
1 | from util.Bilibili import Bilibili
2 | from util.Captcha import Captcha
3 | from util.Config import Config
4 | from util.Data import Data
5 | from util.Geetest import Geetest
6 | from util.Info import Info
7 | from util.Login import Login
8 | from util.Notice import Notice
9 | from util.Request import Request
10 | from util.Task import Task
11 |
12 | __all__ = [
13 | "Bilibili",
14 | "Captcha",
15 | "Config",
16 | "Data",
17 | "Geetest",
18 | "Info",
19 | "Login",
20 | "Notice",
21 | "Request",
22 | "Task",
23 | ]
24 |
--------------------------------------------------------------------------------
/util/Geetest/__init__.py:
--------------------------------------------------------------------------------
1 | from loguru import logger
2 |
3 |
4 | class Geetest:
5 | """
6 | 极验
7 | """
8 |
9 | @logger.catch
10 | def __init__(self):
11 | """
12 | 初始化
13 | """
14 |
15 | @logger.catch
16 | def GetType(self) -> None:
17 | """
18 | Geetest GetType
19 | """
20 |
21 | @logger.catch
22 | def Get1(self) -> None:
23 | """
24 | Geetest 第一次Get
25 | """
26 | pass
27 |
28 | @logger.catch
29 | def Ajax1(self) -> None:
30 | """
31 | Geetest 第一次Ajax
32 | """
33 | pass
34 |
35 | @logger.catch
36 | def Get2(self) -> None:
37 | """
38 | Geetest 第二次Get
39 | """
40 | pass
41 |
42 | @logger.catch
43 | def Ajax2(self) -> None:
44 | """
45 | Geetest 第二次Ajax
46 | """
47 | pass
48 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: https://github.com/pre-commit/pre-commit-hooks
3 | rev: v4.6.0
4 | hooks:
5 | - id: trailing-whitespace
6 | - id: end-of-file-fixer
7 | - id: check-builtin-literals
8 | - id: check-case-conflict
9 | - id: check-docstring-first
10 | - id: debug-statements
11 | - id: check-yaml
12 | - id: check-added-large-files
13 | - id: fix-byte-order-marker
14 | - id: mixed-line-ending
15 |
16 | - repo: https://github.com/asottile/pyupgrade
17 | rev: v3.16.0
18 | hooks:
19 | - id: pyupgrade
20 | args: [--py311-plus]
21 |
22 | - repo: https://github.com/PyCQA/isort
23 | rev: 5.13.2
24 | hooks:
25 | - id: isort
26 |
27 | - repo: https://github.com/psf/black-pre-commit-mirror
28 | rev: 24.4.2
29 | hooks:
30 | - id: black
31 |
32 | - repo: https://github.com/PyCQA/flake8
33 | rev: 7.0.0
34 | hooks:
35 | - id: flake8
36 | entry: pflake8
37 | additional_dependencies: [pyproject-flake8]
38 |
39 | ci:
40 | autoupdate_schedule: weekly
41 | skip: []
42 | submodules: false
43 |
--------------------------------------------------------------------------------
/flake.nix:
--------------------------------------------------------------------------------
1 | {
2 | description = "Python Shell";
3 | inputs = {
4 | nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
5 | flake-utils.url = "github:numtide/flake-utils";
6 | };
7 |
8 | outputs =
9 | {
10 | self,
11 | nixpkgs,
12 | flake-utils,
13 | }:
14 | flake-utils.lib.eachDefaultSystem (
15 | system:
16 | let
17 | pkgs = import nixpkgs { inherit system; };
18 | in
19 | {
20 | devShell =
21 | with pkgs;
22 | mkShell rec {
23 | venvDir = ".venv";
24 | packages =
25 | with pkgs;
26 | [
27 | python312
28 | poetry
29 | portaudio
30 | stdenv.cc.cc.lib
31 | graphviz
32 | ]
33 | ++ (with pkgs.python312Packages; [
34 | pip
35 | venvShellHook
36 | ]);
37 | PIP_INDEX_URL = "https://pypi.tuna.tsinghua.edu.cn/simple";
38 | PIP_TRUSTED_HOST = "pypi.tuna.tsinghua.edu.cn";
39 | NPM_CONFIG_REGISTRY = "https://registry.npmmirror.com";
40 | LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath packages;
41 | };
42 | }
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: Build and Release
2 |
3 | on:
4 | push:
5 | tags:
6 | - "v*.*.*"
7 | pull_request:
8 |
9 | permissions:
10 | contents: write
11 | packages: write
12 | pull-requests: write
13 |
14 | jobs:
15 | build:
16 | runs-on: ${{ matrix.os }}
17 |
18 | strategy:
19 | matrix:
20 | os: [windows-latest, macos-latest]
21 |
22 | steps:
23 | - name: Checkout
24 | uses: actions/checkout@v4
25 |
26 | - name: Install dependencies on macOS
27 | if: matrix.os == 'macos-latest'
28 | run: |
29 | brew update
30 | brew install portaudio
31 |
32 | - name: Install Python
33 | uses: actions/setup-python@v5
34 | with:
35 | python-version: '3.11'
36 |
37 | - name: Install Poetry
38 | uses: abatilo/actions-poetry@v2
39 |
40 | - name: Virturl Environment
41 | run: |
42 | poetry config virtualenvs.create true --local
43 | poetry config virtualenvs.in-project true --local
44 |
45 | - name: Install Dependencies
46 | run: poetry install
47 |
48 | - name: Package
49 | run: poetry run pyinstaller --clean --noconfirm --log-level WARN cli.spec
50 |
51 | - name: Release
52 | uses: softprops/action-gh-release@v2
53 | if: startsWith(github.ref, 'refs/tags/')
54 | with:
55 | prerelease: true
56 | files: dist/*
57 | generate_release_notes: true
58 |
--------------------------------------------------------------------------------
/cli.spec:
--------------------------------------------------------------------------------
1 | # -*- mode: python ; coding: utf-8 -*-
2 | from PyInstaller.utils.hooks import collect_data_files
3 | from PyInstaller.utils.hooks import copy_metadata
4 | import platform
5 |
6 | hiddenimports = []
7 | if platform.system() == "Linux":
8 | name="ticket-Linux"
9 | hiddenimports = ["plyer.platforms.linux.notification"]
10 | elif platform.system() == "Windows":
11 | name="ticket-Windows"
12 | hiddenimports = ["plyer.platforms.win.notification"]
13 | elif platform.system() == "Darwin":
14 | name="ticket-MacOS"
15 | hiddenimports = ["plyer.platforms.macosx.notification"]
16 |
17 | datas = [("assest", "assest")]
18 | datas += collect_data_files("fake_useragent")
19 | datas += copy_metadata("readchar")
20 |
21 |
22 | a = Analysis(
23 | ["cli.py"],
24 | pathex=[],
25 | binaries=[],
26 | datas=datas,
27 | hiddenimports=hiddenimports,
28 | hookspath=[],
29 | hooksconfig={},
30 | runtime_hooks=[],
31 | excludes=[],
32 | noarchive=False,
33 | optimize=0,
34 | )
35 | pyz = PYZ(a.pure)
36 |
37 | exe = EXE(
38 | pyz,
39 | a.scripts,
40 | a.binaries,
41 | a.datas,
42 | [],
43 | name=name,
44 | debug=False,
45 | bootloader_ignore_signals=False,
46 | strip=False,
47 | upx=False,
48 | runtime_tmpdir=None,
49 | console=True,
50 | disable_windowed_traceback=False,
51 | argv_emulation=False,
52 | target_arch=None,
53 | codesign_identity=None,
54 | entitlements_file=None,
55 | icon=["assest\\icon.ico"],
56 | )
57 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.poetry]
2 | name = "bilibili-show-python"
3 | version = "1.0.0"
4 | description = "B站会员购脚本"
5 | authors = ["bilibili-ticket"]
6 | license = "GPL3.0"
7 | readme = "README.md"
8 | package-mode = false
9 |
10 | [tool.poetry.dependencies]
11 | python = ">=3.10,<3.13"
12 | transitions = "^0.9.1"
13 | httpx = {extras = ["http2", "socks"], version = "^0.27.0"}
14 | hishel = "^0.0.27"
15 | fake-useragent = "^1.5.1"
16 | loguru = "^0.7.2"
17 | bili-ticket-gt-python = "^0.2.2"
18 | pycryptodome = "^3.20.0"
19 | pyyaml = "^6.0.1"
20 | inquirer = "^3.2.4"
21 | qrcode = "^7.4.2"
22 | selenium = "^4.21.0"
23 | pybrowsers = "^0.6.0"
24 | py-machineid = "^0.5.1"
25 | plyer = "^2.1.0"
26 | pyaudio = "^0.2.14"
27 | pytz = "^2024.1"
28 | pyinstaller = "^6.7.0"
29 |
30 | [tool.poetry.group.dev]
31 | optional = true
32 |
33 | [tool.poetry.group.dev.dependencies]
34 | pre-commit = "^3.7.1"
35 | isort = "^5.13.2"
36 | black = "^24.4.2"
37 | flake8 = "^7.0.0"
38 | pyupgrade = "^3.15.2"
39 | snakeviz = "^2.2.0"
40 |
41 | [tool.poetry.group.graph]
42 | optional = true
43 |
44 | [tool.poetry.group.graph.dependencies]
45 | graphviz = "^0.20.3"
46 | pygraphviz = "^1.13"
47 |
48 | [tool.poetry.group.doc]
49 | optional = true
50 |
51 | [tool.poetry.group.doc.dependencies]
52 | sphinx = "^7.3.7"
53 | recommonmark = "^0.7.1"
54 |
55 | [tool.isort]
56 | profile = "black"
57 | line_length = 180
58 | skip = [".git", "__pycache__", ".venv",".direnv"]
59 |
60 | [tool.black]
61 | line-length = 180
62 | skip = [".git", "__pycache__", ".venv",".direnv"]
63 |
64 | [tool.flake8]
65 | max-line-length = 180
66 | exclude = [".git", "__pycache__", ".venv",".direnv"]
67 |
68 | [build-system]
69 | requires = ["poetry-core"]
70 | build-backend = "poetry.core.masonry.api"
71 |
--------------------------------------------------------------------------------
/flake.lock:
--------------------------------------------------------------------------------
1 | {
2 | "nodes": {
3 | "flake-utils": {
4 | "inputs": {
5 | "systems": "systems"
6 | },
7 | "locked": {
8 | "lastModified": 1710146030,
9 | "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
10 | "owner": "numtide",
11 | "repo": "flake-utils",
12 | "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
13 | "type": "github"
14 | },
15 | "original": {
16 | "owner": "numtide",
17 | "repo": "flake-utils",
18 | "type": "github"
19 | }
20 | },
21 | "nixpkgs": {
22 | "locked": {
23 | "lastModified": 1718530797,
24 | "narHash": "sha256-pup6cYwtgvzDpvpSCFh1TEUjw2zkNpk8iolbKnyFmmU=",
25 | "owner": "nixos",
26 | "repo": "nixpkgs",
27 | "rev": "b60ebf54c15553b393d144357375ea956f89e9a9",
28 | "type": "github"
29 | },
30 | "original": {
31 | "owner": "nixos",
32 | "ref": "nixos-unstable",
33 | "repo": "nixpkgs",
34 | "type": "github"
35 | }
36 | },
37 | "root": {
38 | "inputs": {
39 | "flake-utils": "flake-utils",
40 | "nixpkgs": "nixpkgs"
41 | }
42 | },
43 | "systems": {
44 | "locked": {
45 | "lastModified": 1681028828,
46 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
47 | "owner": "nix-systems",
48 | "repo": "default",
49 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
50 | "type": "github"
51 | },
52 | "original": {
53 | "owner": "nix-systems",
54 | "repo": "default",
55 | "type": "github"
56 | }
57 | }
58 | },
59 | "root": "root",
60 | "version": 7
61 | }
62 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # B站会员购 蹲票脚本
2 |
3 | [](https://github.com/bilibili-ticket/bilibili-ticket-python/releases)
4 | [](https://github.com/bilibili-ticket/bilibili-ticket-python/actions/workflows/ci.yml)
5 |
6 | > 目前处于开发阶段, 我们无法保证软件的稳定性!
7 |
8 | ## 声明
9 |
10 | [电报交流群](https://t.me/bilibili_ticket)
11 |
12 | 本程序仅供学习交流, 不得用于商业用途,
13 |
14 | 使用本程序进行违法操作产生的法律责任由操作者自行承担,
15 |
16 | 对本程序进行二次开发/分发时请注意遵守GPL-3.0开源协议,
17 |
18 | 本脚本仅适用于蹲回流票, 我们反对将其用于抢票.
19 |
20 | ## 使用
21 |
22 | [下载地址](https://github.com/bilibili-ticket/bilibili-ticket-python/releases)
23 |
24 | 注意:
25 |
26 | 1. 现仅支持部分活动, 主要是类似于BW2024这样的*实名制 一人一票 无选座*活动, 后期会增加更多类型的票务支持;
27 | 2. 如使用浏览器登录功能, 您的电脑里必须安装Chrome/Edge/Firefox浏览器, 如有安装还是提供无法启动, 则需要自行安装其中一个浏览器的Web Driver.
28 |
29 | ```bash
30 | pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
31 | pip install poetry virtualenv
32 |
33 | virtualenv venv
34 | source venv/script/activate
35 | poetry install
36 | python cli.py
37 | ```
38 |
39 | ## 运行流程
40 |
41 | 
42 |
43 | ## 开发计划
44 |
45 | - 1.0.0 Release (BW开票前)
46 | - [ ] 抢票流程细节补充/修复
47 |
48 | - 1.1.0 (BW开票期间)
49 | - [ ] 命令行账号密码/短信验证登录
50 | - [ ] 账号密码登录 二次手机验证码
51 | - [ ] 手机验证码登录
52 | - [ ] 文档
53 |
54 | - 1.2.0
55 | - [ ] 解析JS + biliTicker_gt
56 | - [ ] 刷新Cookie
57 | - [ ] 手动验证
58 |
59 | - 1.3.0
60 | - [ ] 多种类型活动抢票
61 | - [ ] 图形界面(PySide6)
62 | - [ ] Pylint
63 |
64 | - 1.4.0
65 | - [ ] Header补充
66 | - [ ] 注销Cookie
67 | - [ ] Docker(Headless)
68 |
69 | - 1.5.0
70 | - [ ] 网页界面(Gradio)
71 | - [ ] UPX
72 |
73 | ## 开发
74 |
75 | - Python >=3.10,<3.13
76 |
77 | ```bash
78 | pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
79 | pip install poetry virtualenv
80 |
81 | virtualenv venv
82 | source venv/script/activate
83 | poetry install --with dev,doc,graph
84 | pre-commit install
85 |
86 | # 更新
87 | poetry update
88 | pre-commit autoupdate
89 |
90 | # 打包
91 | pyinstaller --clean --noconfirm --log-level WARN cli.spec
92 | ```
93 |
--------------------------------------------------------------------------------
/util/Notice/__init__.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from os import getcwd, path
3 |
4 | from loguru import logger
5 |
6 | from util.Request import Request
7 |
8 |
9 | class Notice:
10 | """
11 | 提示
12 | """
13 |
14 | @logger.catch
15 | def __init__(
16 | self,
17 | title: str,
18 | message: str,
19 | ) -> None:
20 | """
21 | 初始化
22 |
23 | title: 标题
24 | message: 消息
25 | """
26 | self.appName = "Bilibili_Show_Python"
27 | self.appIconPath = self.AssestDir("assest/icon.ico")
28 | self.audioPath = self.AssestDir("assest/alarm.wav")
29 |
30 | self.title = title
31 | self.message = message
32 |
33 | @logger.catch
34 | def AssestDir(self, dir: str):
35 | """
36 | 获取资源文件夹(涉及到Pyinstaller)
37 | """
38 | try:
39 | base_path = sys._MEIPASS # type: ignore
40 | except AttributeError:
41 | base_path = getcwd()
42 | return path.join(base_path, dir)
43 |
44 | @logger.catch
45 | def Message(self, timeout: int = 10) -> None:
46 | """
47 | 弹窗
48 | """
49 | from plyer import notification
50 |
51 | notification.notify(
52 | title=self.title,
53 | message=self.message,
54 | app_icon=self.appIconPath,
55 | app_name=self.appName,
56 | timeout=timeout,
57 | ) # type: ignore
58 |
59 | @logger.catch
60 | def Sound(self, time: int = 2) -> None:
61 | """
62 | 声音
63 | """
64 | import pyaudio
65 |
66 | p = pyaudio.PyAudio()
67 | stream = p.open(format=pyaudio.paInt16, channels=1, rate=44100, output=True)
68 |
69 | for _ in range(time):
70 | stream.write(open(self.audioPath, "rb").read())
71 |
72 | stream.stop_stream()
73 | stream.close()
74 |
75 | @logger.catch
76 | def PushPlus(self, token: str) -> None:
77 | """
78 | PushPlus
79 |
80 | 文档: https://pushplus.plus/doc/
81 |
82 | link: 需要用户点击跳转的链接
83 | """
84 | url = "http://www.pushplus.plus/send"
85 | data = {
86 | "token": token,
87 | "title": self.title,
88 | "content": self.message,
89 | "template": "html",
90 | "channel": "wechat",
91 | }
92 | Request().Response(method="post", url=url, params=data)
93 |
--------------------------------------------------------------------------------
/util/Config/__init__.py:
--------------------------------------------------------------------------------
1 | import glob
2 | from os import getcwd, makedirs, path
3 | from sys import exit
4 |
5 | import yaml
6 | from loguru import logger
7 |
8 | from util.Data import Data
9 |
10 |
11 | class Config:
12 | """
13 | 配置
14 | """
15 |
16 | @logger.catch
17 | def __init__(self, dir: str):
18 | """
19 | 初始化
20 |
21 | dir: 目录
22 | """
23 | self.rootDir = getcwd()
24 | self.dir = path.join(self.rootDir, f"config/{dir}")
25 |
26 | @staticmethod
27 | def dict_to_yaml_str(data: dict) -> str:
28 | """
29 | 将dict转换为YAML格式的str
30 | """
31 | return yaml.dump(data, default_flow_style=False)
32 |
33 | @staticmethod
34 | def yaml_str_to_dict(yaml_str: str) -> dict:
35 | """
36 | 将YAML格式的str转换为dict
37 | """
38 | return yaml.load(yaml_str, Loader=yaml.FullLoader)
39 |
40 | @logger.catch
41 | def List(self) -> list:
42 | """
43 | 列表
44 | """
45 | try:
46 | if not path.exists(self.dir):
47 | makedirs(self.dir)
48 |
49 | files = glob.glob(path.join(self.dir, "*.yaml"))
50 | files = [path.splitext(path.basename(file))[0] for file in files]
51 | return files
52 |
53 | except Exception as e:
54 | logger.exception(f"【配置】读取配置列表错误! {e}")
55 | exit()
56 |
57 | @logger.catch
58 | def Load(self, filename: str, decrypt: bool = False) -> dict:
59 | """
60 | 读取
61 |
62 | decrypt: 是否解密
63 | """
64 | try:
65 | with open(f"{self.dir}/{filename}.yaml", encoding="utf-8") as file:
66 | if decrypt:
67 | yaml_str = file.read()
68 | decrypted_yaml_str = Data().AESDecrypt(yaml_str)
69 | return self.yaml_str_to_dict(decrypted_yaml_str)
70 | else:
71 | return yaml.load(file, Loader=yaml.FullLoader)
72 |
73 | except FileNotFoundError:
74 | logger.exception(f"【配置】读取的配置文件 {filename} 不存在!")
75 | return {}
76 |
77 | except yaml.YAMLError as e:
78 | logger.exception(f"【配置】读取配置文件错误!{e}")
79 | return {}
80 |
81 | @logger.catch
82 | def Save(self, filename: str, data: dict, encrypt: bool = False) -> None:
83 | """
84 | 写入并加密
85 |
86 | data: 数据
87 | encrypt: 是否加密
88 | """
89 | try:
90 | with open(f"{self.dir}/{filename}.yaml", "w", encoding="utf-8") as file:
91 | if encrypt:
92 | yaml_str = self.dict_to_yaml_str(data)
93 | encrypted_yaml_str = Data().AESEncrypt(yaml_str)
94 | file.write(encrypted_yaml_str)
95 | else:
96 | yaml.dump(data, file, allow_unicode=True)
97 |
98 | except Exception as e:
99 | logger.exception(f"【配置】保存配置文件错误!{e}")
100 |
--------------------------------------------------------------------------------
/util/Captcha/__init__.py:
--------------------------------------------------------------------------------
1 | import time
2 |
3 | from bili_ticket_gt_python import ClickPy, SlidePy
4 | from loguru import logger
5 |
6 |
7 | class Captcha:
8 | """
9 | 验证
10 | """
11 |
12 | @logger.catch
13 | def __init__(
14 | self,
15 | verify: SlidePy | ClickPy | None = None,
16 | gt: str = "ac597a4506fee079629df5d8b66dd4fe",
17 | ):
18 | """
19 | 初始化
20 |
21 | log: 日志实例
22 | verify: 验证码实例
23 | gt: 极验gt
24 | """
25 | self.verify = verify
26 | self.gt = gt
27 |
28 | self.rt = "abcdefghijklmnop" # rt固定即可
29 |
30 | @logger.catch
31 | def Geetest(self, challenge: str) -> str:
32 | """
33 | 极验自动验证
34 | https://github.com/Amorter/biliTicker_gt
35 |
36 | challenge: 流水号
37 | 返回: validate
38 | """
39 | if isinstance(self.verify, ClickPy):
40 | return self.Auto(challenge)
41 | elif isinstance(self.verify, SlidePy):
42 | return self.Slide(challenge)
43 | else:
44 | raise Exception("未指定验证码实例或实例类型不正确")
45 |
46 | @logger.catch
47 | def Auto(self, challenge: str) -> str:
48 | """
49 | 极验文字点选 - 自动重试
50 |
51 | challenge: 流水号
52 | 返回: validate
53 | """
54 | try:
55 | validate = self.verify.simple_match_retry(self.gt, challenge) # type: ignore
56 | return validate
57 | except Exception:
58 | raise
59 |
60 | @logger.catch
61 | def Click(self, challenge: str) -> str:
62 | """
63 | 极验文字点选
64 |
65 | challenge: 流水号
66 | 返回: validate
67 | """
68 | try:
69 | c, s, args = self.verify.get_new_c_s_args(self.gt, challenge) # type: ignore
70 | before_calculate_key = time.time()
71 | key = self.verify.calculate_key(args) # type: ignore
72 | w = self.verify.generate_w(key, self.gt, challenge, str(c), s, self.rt) # type: ignore
73 | # 点选验证码生成w后需要等待2秒提交
74 | w_use_time = time.time() - before_calculate_key
75 | if w_use_time < 2:
76 | time.sleep(2 - w_use_time)
77 | msg, validate = self.verify.verify(self.gt, challenge, w) # type: ignore
78 | logger.info(f"【验证码】验证结果: {msg}")
79 | return validate
80 | except Exception:
81 | raise
82 |
83 | @logger.catch
84 | def Slide(self, challenge: str) -> str:
85 | """
86 | 极验滑块
87 |
88 | challenge: 流水号
89 | 返回: validate
90 | """
91 | try:
92 | c, s, args = self.verify.get_new_c_s_args(self.gt, challenge) # type: ignore
93 | # 注意滑块验证码这里要刷新challenge
94 | challenge = args[0]
95 | key = self.verify.calculate_key(args) # type: ignore
96 | w = self.verify.generate_w(key, self.gt, challenge, str(c), s, self.rt) # type: ignore
97 | msg, validate = self.verify.verify(self.gt, challenge, w) # type: ignore
98 | logger.info(f"【验证码】验证结果: {msg}")
99 | return validate
100 | except Exception:
101 | raise
102 |
103 | @logger.catch
104 | def Manual(self) -> str:
105 | """
106 | 手动验证
107 | """
108 | validate = ""
109 | return validate
110 |
--------------------------------------------------------------------------------
/cli.py:
--------------------------------------------------------------------------------
1 | import os
2 | import shutil
3 | import threading
4 |
5 | from bili_ticket_gt_python import ClickPy
6 | from loguru import logger
7 |
8 | from interface import ProductCli, SettingCli, UserCli
9 | from util import Captcha, Config, Notice, Request, Task
10 |
11 | if __name__ == "__main__":
12 | # 丢锅
13 | print(
14 | """
15 | |=====================================================================
16 | |
17 | | 欢迎使用https://github.com/bilibili-ticket/bilibili-ticket-python
18 | | 本程序仅供学习交流, 不得用于商业用途
19 | | 使用本程序进行违法操作产生的法律责任由操作者自行承担
20 | | 对本程序进行二次开发/分发时请注意遵守GPL-3.0开源协议
21 | | 本脚本仅适用于蹲回流票, 我们反对将其用于抢票
22 | |
23 | |=====================================================================
24 | |
25 | | 交互: 上下 键盘↑↓键, 多选 空格, 确认 回车
26 | |
27 | |=====================================================================
28 | """
29 | )
30 |
31 | # 日志
32 | logger.add(
33 | "log/{time}.log",
34 | colorize=True,
35 | enqueue=True,
36 | encoding="utf-8",
37 | # 日志保留天数
38 | retention=3,
39 | # 调试
40 | backtrace=True,
41 | diagnose=True,
42 | )
43 |
44 | # 删除缓存
45 | if os.path.exists(".cache"):
46 | shutil.rmtree(".cache")
47 |
48 | # 初始化
49 | # 用户数据文件
50 | userData = Config(dir="user")
51 | productData = Config(dir="product")
52 | settingData = Config(dir="setting")
53 | # 验证
54 | verify = ClickPy()
55 | cap = Captcha(verify=verify)
56 |
57 | # 检测配置文件情况
58 | userList = userData.List()
59 | productList = productData.List()
60 | settingList = settingData.List()
61 |
62 | # 读取配置
63 | if userList != []:
64 | userConfig = UserCli(conf=userData).Select(selects=userList)
65 | else:
66 | userConfig = UserCli(conf=userData).Generate()
67 |
68 | if productList != []:
69 | productConfig = ProductCli(conf=productData).Select(selects=productList)
70 | else:
71 | productConfig = ProductCli(conf=productData).Generate()
72 |
73 | if settingList != []:
74 | settingConfig = SettingCli(conf=settingData).Select(selects=settingList)
75 | else:
76 | settingConfig = SettingCli(conf=settingData).Generate()
77 |
78 | net = Request(
79 | cookie=userConfig["cookie"],
80 | header=userConfig["header"],
81 | timeout=settingConfig["request"]["timeout"],
82 | retry=settingConfig["request"]["retry"],
83 | proxy=settingConfig["request"]["proxy"],
84 | )
85 |
86 | job = Task(
87 | net=net,
88 | cap=cap,
89 | sleep=settingConfig["request"]["sleep"],
90 | projectId=productConfig["projectId"],
91 | screenId=productConfig["screenId"],
92 | skuId=productConfig["skuId"],
93 | buyer=userConfig["buyer"],
94 | )
95 |
96 | # job.DrawFSM()
97 |
98 | # 任务流
99 | if job.Run():
100 | notice = Notice(title="抢票", message="下单成功! 请在十分钟内支付")
101 | mode = settingConfig["notice"]
102 | logger.info("【抢票】下单成功! 请在十分钟内支付")
103 |
104 | # 多线程通知
105 | noticeThread = []
106 | t1 = threading.Thread(target=notice.Message)
107 | t2 = threading.Thread(target=notice.Sound)
108 | t3 = threading.Thread(target=notice.PushPlus, args=(mode["plusPush"],))
109 |
110 | if mode["system"]:
111 | noticeThread.append(t1)
112 | if mode["sound"]:
113 | noticeThread.append(t2)
114 | if mode["wechat"]:
115 | noticeThread.append(t3)
116 |
117 | for t in noticeThread:
118 | t.start()
119 |
--------------------------------------------------------------------------------
/interface/CLI/product.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 | from loguru import logger
4 |
5 | from util import Config, Data, Info, Request
6 |
7 |
8 | class ProductCli:
9 | """
10 | 商品配置交互
11 | """
12 |
13 | @logger.catch
14 | def __init__(self, conf: Config):
15 | """
16 | 初始化
17 |
18 | conf: 配置实例
19 | """
20 | self.conf = conf
21 |
22 | self.data = Data()
23 | self.net = Request()
24 | self.info = Info(net=self.net)
25 |
26 | # 配置
27 | self.config = {
28 | # 活动ID
29 | "projectId": 0,
30 | # 场次ID
31 | "screenId": 0,
32 | # 价格ID
33 | "skuId": 0,
34 | }
35 |
36 | @logger.catch
37 | def Select(self, selects: list) -> dict:
38 | """
39 | 选择配置
40 |
41 | selects: 可选择项目
42 | """
43 | selects.append("新建配置")
44 | select = self.data.Inquire(type="List", message="请选择加载的商品配置", choices=selects)
45 |
46 | if select == "新建配置":
47 | return self.Generate()
48 |
49 | else:
50 | self.config = self.conf.Load(filename=select)
51 | return self.config
52 |
53 | @logger.catch
54 | def Generate(self) -> dict:
55 | """
56 | 生成配置
57 | """
58 |
59 | @logger.catch
60 | def ProjectStep() -> int:
61 | """
62 | 活动
63 | """
64 | print("[!] BW2024链接: show.bilibili.com/platform/detail.html?id=85939")
65 | url = self.data.Inquire(
66 | type="Text",
67 | message="请粘贴要抢的活动的网页链接",
68 | )
69 |
70 | match = re.search(r"id=(\d+)", url)
71 | if match:
72 | projectId = match.group(1)
73 | return int(projectId)
74 |
75 | else:
76 | logger.error("【商品配置初始化】活动URL格式错误!")
77 | return ProjectStep()
78 |
79 | @logger.catch
80 | def ScrenStep() -> int:
81 | """
82 | 场次
83 | """
84 | projectInfo = self.info.Project()
85 | screenInfo = self.info.Screen()
86 |
87 | lists = {f"{screenInfo[i]['name']} ({screenInfo[i]['display_name']})": screenInfo[i]["id"] for i in screenInfo}
88 | select = self.data.Inquire(
89 | type="List",
90 | message=f"您选择的活动是:{projectInfo['name']}, 接下来请选择场次",
91 | choices=list(lists.keys()),
92 | )
93 | return lists[select]
94 |
95 | @logger.catch
96 | def SkuStep(screenId: int) -> int:
97 | """
98 | 价位
99 |
100 | screenId: 场次ID
101 | """
102 | skuInfo = self.info.Sku(screenId)
103 | lists = {(f"{skuInfo[i]['name']} {skuInfo[i]['price']}元 " f"({skuInfo[i]['display_name']})"): skuInfo[i]["id"] for i in skuInfo}
104 | select = self.data.Inquire(
105 | type="List",
106 | message="请选择价位",
107 | choices=list(lists.keys()),
108 | )
109 | return lists[select]
110 |
111 | @logger.catch
112 | def FilenameStep(name: str) -> str:
113 | """
114 | 文件名
115 |
116 | skuid: 价位ID
117 | """
118 | filename = self.data.Inquire(
119 | type="Text",
120 | message="保存的商品文件名称",
121 | default=name,
122 | )
123 | return filename
124 |
125 | print("下面开始配置商品!")
126 | self.config["projectId"] = ProjectStep()
127 | self.info = Info(net=self.net, pid=self.config["projectId"])
128 | self.config["screenId"] = ScrenStep()
129 | self.config["skuId"] = SkuStep(screenId=self.config["screenId"])
130 |
131 | self.conf.Save(FilenameStep(name=self.info.Project()["name"]), self.config)
132 | logger.info("【商品配置初始化】配置已保存!")
133 | return self.config
134 |
--------------------------------------------------------------------------------
/interface/CLI/user.py:
--------------------------------------------------------------------------------
1 | from sys import exit
2 |
3 | from loguru import logger
4 |
5 | from util import Config, Data, Info, Login, Request
6 |
7 |
8 | class UserCli:
9 | """
10 | 用户配置交互
11 | """
12 |
13 | @logger.catch
14 | def __init__(self, conf: Config):
15 | """
16 | 初始化
17 |
18 | conf: 配置实例
19 | """
20 | self.conf = conf
21 |
22 | self.data = Data()
23 | self.net = Request()
24 |
25 | # 配置
26 | self.config = {
27 | # Cookie
28 | "cookie": {},
29 | # Header
30 | "header": {},
31 | # 购买人
32 | "buyer": {},
33 | }
34 |
35 | @logger.catch
36 | def Select(self, selects: list) -> dict:
37 | """
38 | 选择配置
39 |
40 | selects: 可选择项目
41 | """
42 | selects.append("新建用户配置")
43 | select = self.data.Inquire(type="List", message="请选择加载的用户配置", choices=selects)
44 |
45 | if select == "新建用户配置":
46 | return self.Generate()
47 |
48 | else:
49 | self.config = self.conf.Load(filename=select, decrypt=True)
50 | net = Request(cookie=self.config["cookie"], header=self.config["header"])
51 | Login(net=net).Status()
52 | return self.config
53 |
54 | @logger.catch
55 | def Generate(self) -> dict:
56 | """
57 | 生成配置
58 | """
59 |
60 | @logger.catch
61 | def LoginStep() -> dict:
62 | """
63 | 登录
64 | """
65 | login = Login(net=self.net)
66 | mode = self.data.Inquire(
67 | type="List",
68 | message="请选择B站账号登录模式",
69 | choices=["扫描二维码", "浏览器登录", "账号密码登录", "手机验证码登录", "手动输入Cookie"],
70 | )
71 |
72 | match mode:
73 | case "扫描二维码":
74 | print("请使用B站手机客户端扫描二维码, 如果命令行内二维码无法正常显示, 请打开软件目录下的 qr.jpg 进行扫描")
75 | return login.QRCode()
76 |
77 | case "浏览器登录":
78 | return login.Selenium()
79 |
80 | case "账号密码登录":
81 | username = self.data.Inquire(
82 | type="Text",
83 | message="请输入B站账号",
84 | )
85 | password = self.data.Inquire(
86 | type="Password",
87 | message="请输入B站密码",
88 | )
89 | return login.Password(username=username, password=password)
90 |
91 | case "手机验证码登录":
92 | tel = self.data.Inquire(
93 | type="Text",
94 | message="请输入手机号",
95 | )
96 | try:
97 | captcha_key = login.SMSSend(tel)
98 | if captcha_key:
99 | code = self.data.Inquire(
100 | type="Text",
101 | message="请输入验证码",
102 | )
103 | return login.SMSVerify(tel=tel, code=code, captcha_key=captcha_key)
104 | else:
105 | raise Exception("验证码发送失败!")
106 | except Exception as e:
107 | logger.exception(f"【登录】登录错误 {e}")
108 | return LoginStep()
109 |
110 | case "手动输入Cookie":
111 | cookie = self.data.Inquire(
112 | type="Text",
113 | message="请输入Cookie",
114 | )
115 | return login.Cookie(cookie=cookie)
116 |
117 | case _:
118 | logger.error("【登录】未知登录模式!")
119 | exit()
120 |
121 | @logger.catch
122 | def BuyerStep() -> dict:
123 | """
124 | 购买人
125 | """
126 | buyerInfo = Info(net=self.net).Buyer()
127 | choice = {f"{i['购买人']} - {i['身份证']} - {i['手机号']}": x for x, i in enumerate(buyerInfo)}
128 |
129 | select = self.data.Inquire(
130 | type="List",
131 | message="请选择购票人",
132 | choices=list(choice.keys()),
133 | )
134 |
135 | id = choice[select]
136 | dist = buyerInfo[id]["数据"]
137 | return dist
138 |
139 | @logger.catch
140 | def FilenameStep(name: str) -> str:
141 | """
142 | 文件名
143 |
144 | name: 实名名称
145 | """
146 | filename = self.data.Inquire(
147 | type="Text",
148 | message="保存的用户文件名称",
149 | default=name[0] + "X" + name[-1],
150 | )
151 | return filename
152 |
153 | print("下面开始配置用户!")
154 | self.config["cookie"] = LoginStep()
155 | self.config["header"] = self.net.GetHeader()
156 | self.config["buyer"] = BuyerStep()
157 | self.conf.Save(FilenameStep(name=self.config["buyer"]["name"]), self.config, encrypt=True)
158 | return self.config
159 |
--------------------------------------------------------------------------------
/util/Request/__init__.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from sys import exit
3 |
4 | import hishel
5 | import httpx
6 | from fake_useragent import UserAgent
7 | from loguru import logger
8 |
9 |
10 | class Request:
11 | """
12 | 网络请求
13 | """
14 |
15 | @logger.catch
16 | def __init__(
17 | self,
18 | cookie: dict = {},
19 | header: dict = {},
20 | timeout: float = 5.0,
21 | retry: int = 3,
22 | proxy: str | None = None,
23 | redirect: bool = True,
24 | isDebug: bool = False,
25 | ):
26 | """
27 | 初始化
28 |
29 | cookie: Dict Cookie
30 | timeout: 超时
31 | retry: 重试次数
32 | proxy: 代理
33 | redirect: 重定向
34 | isDebug: 调试模式
35 | """
36 |
37 | self.cookie = cookie
38 | self.timeout = timeout
39 | self.retry = retry
40 | self.proxy = proxy
41 | self.redirect = redirect
42 | self.isDebug = isDebug
43 |
44 | self.header = {
45 | "Accept": "*/*",
46 | "Accept-Language": "zh-CN,zh;q=0.9",
47 | "Authority": "show.bilibili.com",
48 | "Connection": "keep-alive",
49 | "Referer": "https://show.bilibili.com",
50 | "Origin": "https://show.bilibili.com/",
51 | "Connection": "keep-alive",
52 | "Sec-Fetch-Dest": "empty",
53 | "Sec-Fetch-Mode": "cors",
54 | "Sec-Fetch-Site": "same-origin",
55 | "User-Agent": UserAgent(os="android", platforms="mobile").random,
56 | } | header
57 |
58 | self.session = hishel.CacheClient(
59 | cookies=self.cookie,
60 | headers=self.header,
61 | timeout=self.timeout,
62 | proxy=self.proxy,
63 | # 重定向
64 | follow_redirects=self.redirect,
65 | # HTTP2
66 | http2=True,
67 | # SSL
68 | verify=False,
69 | # Hook
70 | event_hooks={
71 | "request": [self.RequestHook],
72 | "response": [self.ResponseHook],
73 | },
74 | # 缓存
75 | controller=hishel.Controller(
76 | # 缓存请求模式
77 | cacheable_methods=["GET", "POST"],
78 | # 缓存状态码
79 | cacheable_status_codes=[200],
80 | # 无法新连接时读取缓存
81 | allow_stale=False,
82 | # 强制刷新缓存
83 | always_revalidate=True,
84 | ),
85 | )
86 |
87 | # 关闭Httpx自带日志
88 | logging.getLogger("httpx").setLevel(logging.CRITICAL)
89 |
90 | @logger.catch
91 | def Response(self, method: str, url: str, params: dict = {}) -> httpx.Response:
92 | """
93 | 网络
94 |
95 | method: 方法 post/get
96 | url: 地址 str
97 | params: 参数 dict
98 | """
99 | methods = {
100 | "get": self.session.get,
101 | "post": self.session.post,
102 | }
103 |
104 | if method not in methods:
105 | raise
106 |
107 | for _ in range(self.retry):
108 | try:
109 | return methods[method](url=url, **({"params": params} if method == "get" else {"data": params}))
110 |
111 | except httpx.RequestError as e:
112 | logger.exception(f"【网络请求】请求错误: {e}")
113 |
114 | logger.debug("【网络请求】疑似IP被Ban/无网络!")
115 | quit()
116 |
117 | @logger.catch
118 | def GetCookie(self) -> dict:
119 | """
120 | 获取Cookie
121 | """
122 | return dict(self.session.cookies)
123 |
124 | @logger.catch
125 | def GetHeader(self) -> dict:
126 | """
127 | 获取Header
128 | """
129 | return self.header
130 |
131 | @logger.catch
132 | def RefreshCookie(self, cookie: dict) -> None:
133 | """
134 | 刷新Cookie
135 |
136 | cookie: Cookie
137 | """
138 | self.cookie = cookie
139 | self.session.cookies.update(self.cookie)
140 |
141 | @logger.catch
142 | def RequestHook(self, request: httpx.Request) -> None:
143 | """
144 | 请求事件钩子
145 | """
146 | # 调试模式
147 | if self.isDebug:
148 | if "show.bilibili.com" in str(request.url):
149 | logger.debug(f"【Request请求】地址: {request.url} 方法: {request.method} 内容: {request.content} 请求参数: {request.read()}")
150 | else:
151 | logger.debug(f"【Request请求】地址: {request.url} 方法: {request.method}")
152 |
153 | @logger.catch
154 | def ResponseHook(self, response: httpx.Response) -> None:
155 | """
156 | 响应事件钩子
157 | """
158 | request = response.request
159 | # 调试模式
160 | if self.isDebug:
161 | if "show.bilibili.com" in str(request.url):
162 | logger.debug(f"【Request响应】地址: {request.url} 状态码: {response.status_code} 返回: {response.read()}")
163 | else:
164 | logger.debug(f"【Request响应】地址: {request.url} 状态码: {response.status_code}")
165 |
166 | # 风控
167 | if response.status_code != 200:
168 | if response.status_code != 412:
169 | if "show.bilibili.com" in str(request.url):
170 | logger.error(f"【Request响应】请求错误, 状态码: {response.status_code}")
171 | else:
172 | logger.error("【Request响应】IP被412风控!!!!! 请更换IP(重启路由器/使用手机流量热点/梯子......)后再次使用")
173 | exit()
174 |
--------------------------------------------------------------------------------
/util/Info/__init__.py:
--------------------------------------------------------------------------------
1 | from sys import exit
2 |
3 | from loguru import logger
4 |
5 | from util import Data, Request
6 |
7 |
8 | class Info:
9 | """
10 | 信息
11 | """
12 |
13 | @logger.catch
14 | def __init__(self, net: Request, pid: int = 0):
15 | """
16 | 初始化
17 |
18 | net: 网络实例
19 | pid: 场次ID
20 | """
21 | self.net = net
22 | self.pid = pid
23 |
24 | self.data = Data()
25 |
26 | @logger.catch
27 | def Project(self) -> dict:
28 | """
29 | 项目基本信息
30 |
31 | 接口: GET https://show.bilibili.com/api/ticket/project/getV2?version=134&id=${pid}
32 | """
33 | url = f"https://show.bilibili.com/api/ticket/project/getV2?version=134&id={self.pid}"
34 | response = self.net.Response(method="get", url=url).json()
35 |
36 | base_info_id = 0
37 | for i in range(len(response["data"]["performance_desc"]["list"])):
38 | if response["data"]["performance_desc"]["list"][i]["module"] == "base_info":
39 | base_info_id = i
40 |
41 | dist = {
42 | "id": response["data"]["id"],
43 | "name": response["data"]["name"],
44 | "time": response["data"]["performance_desc"]["list"][base_info_id]["details"][0]["content"],
45 | "start": self.data.TimestampFormat(int(response["data"]["sale_begin"])),
46 | "end": self.data.TimestampFormat(int(response["data"]["sale_end"])),
47 | "countdown": self.data.TimestampFormat(int(response["data"]["count_down"]), "s", countdown=True),
48 | }
49 | return dist
50 |
51 | @logger.catch
52 | def Screen(self) -> dict:
53 | """
54 | 场次信息
55 |
56 | 接口: GET https://show.bilibili.com/api/ticket/project/getV2?version=134&id=${pid}
57 | """
58 | url = f"https://show.bilibili.com/api/ticket/project/getV2?version=134&id={self.pid}"
59 | response = self.net.Response(method="get", url=url).json()
60 |
61 | screens = response["data"]["screen_list"]
62 | if not screens:
63 | logger.info("【活动详情】该活动暂未开放票务信息")
64 | exit()
65 |
66 | dist = {}
67 | for i in range(len(screens)):
68 | screen = screens[i]
69 | dist[i] = {
70 | "id": screen["id"],
71 | "name": screen["name"],
72 | "display_name": screen["saleFlag"]["display_name"],
73 | "sale_start": self.data.TimestampFormat(int(screen["sale_start"])),
74 | "sale_end": self.data.TimestampFormat(int(screen["sale_end"])),
75 | }
76 | return dist
77 |
78 | @logger.catch
79 | def Sku(self, sid: int) -> dict:
80 | """
81 | 价格信息
82 |
83 | 接口: GET https://show.bilibili.com/api/ticket/project/getV2?version=134&id=${pid}
84 |
85 | sid: 场次ID
86 | """
87 | url = f"https://show.bilibili.com/api/ticket/project/getV2?version=134&id={self.pid}"
88 | response = self.net.Response(method="get", url=url).json()
89 |
90 | skus = {}
91 | for i in response["data"]["screen_list"]:
92 | if i["id"] == sid:
93 | skus = i["ticket_list"]
94 | break
95 |
96 | dist = {}
97 | for i in range(len(skus)):
98 | sku = skus[i]
99 | dist[i] = {
100 | "id": sku["id"],
101 | "name": f"{sku['screen_name']} - {sku['desc']}",
102 | "display_name": sku["sale_flag"]["display_name"],
103 | "price": f"{(sku['price'] / 100):.2f}",
104 | "sale_start": sku["sale_start"],
105 | "sale_end": sku["sale_end"],
106 | }
107 | return dist
108 |
109 | @logger.catch
110 | def Buyer(self) -> list:
111 | """
112 | 购买人
113 |
114 | 接口: GET https://show.bilibili.com/api/ticket/buyer/list?is_default&projectId=${pid}
115 | """
116 | url = "https://show.bilibili.com/api/ticket/buyer/list"
117 | response = self.net.Response(method="get", url=url).json()
118 |
119 | buyers_info = []
120 | lists = response["data"]["list"]
121 |
122 | if len(lists) == 0:
123 | logger.info("【购买人】暂无购买人信息, 请到会员购平台绑定后再次使用!")
124 | exit()
125 |
126 | for i in range(len(lists)):
127 | info = lists[i]
128 |
129 | # 补充/删除信息
130 | info.pop("error_code")
131 | info["buyer"] = None
132 | info["disabledErr"] = None
133 | info["isBuyerInfoVerified"] = True
134 | info["isBuyerValid"] = True
135 |
136 | buyer_name = info["name"]
137 | buyer_id = info["personal_id"]
138 | buyer_tel = info["tel"]
139 | buyer_info = {
140 | "购买人": buyer_name[0] + "*" * 1 + buyer_name[-1],
141 | "身份证": buyer_id[:6] + "*" * 8 + buyer_id[-4:],
142 | "手机号": buyer_tel[:3] + "*" * 4 + buyer_tel[-4:],
143 | "数据": info,
144 | }
145 | buyers_info.append(buyer_info)
146 | return buyers_info
147 |
148 | @logger.catch
149 | def UID(self) -> int:
150 | """
151 | UID
152 |
153 | 接口: GET https://show.bilibili.com/api/ticket/project/getV2?version=134&id=${pid}
154 | """
155 | url = f"https://show.bilibili.com/api/ticket/project/getV2?version=134&id={self.pid}"
156 | response = self.net.Response(method="get", url=url).json()
157 | return response["data"]["mid"]
158 |
--------------------------------------------------------------------------------
/interface/CLI/setting.py:
--------------------------------------------------------------------------------
1 | import re
2 | import time
3 |
4 | from loguru import logger
5 |
6 | from util import Config, Data
7 |
8 |
9 | class SettingCli:
10 | """
11 | 设置配置交互
12 | """
13 |
14 | # 提醒模式
15 | noticeMode = ["系统提醒", "音频提醒", "微信提醒(Push Plus)"]
16 |
17 | @logger.catch
18 | def __init__(self, conf: Config):
19 | """
20 | 初始化
21 |
22 | conf: 配置实例
23 | """
24 | self.conf = conf
25 |
26 | self.data = Data()
27 |
28 | # 配置
29 | self.config = {
30 | # 网络
31 | "request": {
32 | # 请求间隔
33 | "sleep": 1.5,
34 | # 超时
35 | "timeout": 5,
36 | # 重试
37 | "retry": 3,
38 | # 代理
39 | "proxy": None,
40 | },
41 | # 提醒
42 | "notice": {
43 | # 系统提醒
44 | "system": False,
45 | # 音频提醒
46 | "sound": False,
47 | # 微信提醒
48 | "wechat": False,
49 | # Plus Push Token
50 | "plusPush": "",
51 | },
52 | }
53 |
54 | @logger.catch
55 | def Select(self, selects: list) -> dict:
56 | """
57 | 选择配置
58 |
59 | selects: 可选择项目
60 | """
61 | selects.append("新建配置")
62 | select = self.data.Inquire(type="List", message="请选择加载的设置配置", choices=selects)
63 |
64 | if select == "新建配置":
65 | return self.Generate()
66 |
67 | else:
68 | self.config = self.conf.Load(filename=select)
69 | return self.config
70 |
71 | @logger.catch
72 | def Generate(self) -> dict:
73 | """
74 | 生成配置
75 | """
76 |
77 | @logger.catch
78 | def SleepStep() -> float:
79 | """
80 | 请求间隔
81 | """
82 | interval = self.data.Inquire(
83 | type="Text",
84 | message="请输入创建订单请求间隔时间(单位:秒, 建议大于多少来着?)",
85 | default="3",
86 | )
87 | return float(interval)
88 |
89 | @logger.catch
90 | def TimeoutStep() -> float:
91 | """
92 | 超时
93 | """
94 | timeout = self.data.Inquire(
95 | type="Text",
96 | message="请输入请求超时时间(单位:秒)",
97 | default="5",
98 | )
99 | return float(timeout)
100 |
101 | @logger.catch
102 | def RetryStep() -> int:
103 | """
104 | 重试
105 | """
106 | retry = self.data.Inquire(
107 | type="Text",
108 | message="请输入请求重试次数",
109 | default="3",
110 | )
111 | return int(retry)
112 |
113 | @logger.catch
114 | def ProxyStep() -> str | None:
115 | """
116 | 代理
117 | """
118 | select = self.data.Inquire(
119 | type="Confirm",
120 | message="是否使用代理",
121 | default=False,
122 | )
123 | if select:
124 | proxy = self.data.Inquire(
125 | type="Text",
126 | message="请输入代理地址",
127 | default="http://xxxx.xxx:8080",
128 | )
129 | return proxy
130 | return None
131 |
132 | @logger.catch
133 | def NoticeStep() -> tuple[bool, bool, bool, str]:
134 | """
135 | 提醒
136 | """
137 | dist = {
138 | "system": False,
139 | "sound": False,
140 | "wechat": False,
141 | "plusPush": "",
142 | }
143 | select = self.data.Inquire(
144 | type="Checkbox",
145 | message="抢票成功通知方式",
146 | choices=[
147 | ("系统提醒", "system"),
148 | ("音频提醒", "sound"),
149 | ("微信提醒(Push Plus)", "wechat"),
150 | ],
151 | default=["系统提醒", "音频提醒"],
152 | )
153 | for i in select:
154 | dist[i] = True
155 |
156 | if "wechat" in select:
157 | token = self.data.Inquire(
158 | type="Text",
159 | message="请输入Push Plus Token",
160 | default="",
161 | )
162 | dist["plusPush"] = token
163 |
164 | return dist["system"], dist["sound"], dist["wechat"], dist["plusPush"]
165 |
166 | @logger.catch
167 | def FilenameStep() -> str:
168 | """
169 | 文件名
170 | """
171 | default = re.sub(r'[\\/*?:"<>|]', "_", time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))
172 | filename = self.data.Inquire(
173 | type="Text",
174 | message="保存的设置文件名称",
175 | default=default,
176 | )
177 | return filename
178 |
179 | print("下面开始配置设置!")
180 | self.config["request"]["sleep"] = SleepStep()
181 | self.config["request"]["timeout"] = TimeoutStep()
182 | self.config["request"]["retry"] = RetryStep()
183 | self.config["request"]["proxy"] = ProxyStep()
184 | self.config["notice"]["system"], self.config["notice"]["sound"], self.config["notice"]["wechat"], self.config["notice"]["plusPush"] = NoticeStep()
185 |
186 | self.conf.Save(FilenameStep(), self.config)
187 | logger.info("【设置配置初始化】配置已保存!")
188 | return self.config
189 |
--------------------------------------------------------------------------------
/util/Task/__init__.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from time import sleep
3 |
4 | from loguru import logger
5 | from transitions import Machine, State
6 |
7 | from util import Bilibili, Captcha, Request
8 |
9 |
10 | class Task:
11 | """
12 | 状态机
13 | """
14 |
15 | @logger.catch
16 | def __init__(
17 | self,
18 | net: Request,
19 | cap: Captcha,
20 | sleep: int,
21 | projectId: int,
22 | screenId: int,
23 | skuId: int,
24 | buyer: dict,
25 | ):
26 | """
27 | 初始化
28 |
29 | net: 网络实例
30 | cap: 验证码实例
31 | sleep: 任务间请求间隔时间
32 | projectId: 项目ID
33 | screenId: 场次ID
34 | skuId: 商品ID
35 | buyer: 购买者信息
36 | """
37 |
38 | self.net = net
39 | self.cap = cap
40 | self.sleep = sleep
41 | self.api = Bilibili(net=self.net, projectId=projectId, screenId=screenId, skuId=skuId, buyer=buyer)
42 |
43 | self.states = [
44 | State(name="开始"),
45 | State(name="获取Token", on_enter="QueryTokenAction"),
46 | State(name="验证码", on_enter="RiskProcessAction"),
47 | State(name="等待余票", on_enter="QueryTicketAction"),
48 | State(name="创建订单", on_enter="CreateOrderAction"),
49 | State(name="创建订单状态", on_enter="CreateStatusAction"),
50 | State(name="完成"),
51 | ]
52 |
53 | # from transitions.extensions import GraphMachine
54 | self.machine = Machine(
55 | model=self,
56 | states=self.states,
57 | initial="开始",
58 | # show_state_attributes=True,
59 | )
60 |
61 | self.machine.add_transition(
62 | trigger="Next",
63 | source="开始",
64 | dest="获取Token",
65 | )
66 |
67 | # 0-成功, 1-验证码, 2-失败
68 | self.machine.add_transition(
69 | trigger="QueryToken",
70 | source="获取Token",
71 | dest="创建订单",
72 | conditions=lambda: self.queryTokenResult == 0,
73 | )
74 | self.machine.add_transition(
75 | trigger="QueryToken",
76 | source="获取Token",
77 | dest="验证码",
78 | conditions=lambda: self.queryTokenResult == 1,
79 | )
80 | self.machine.add_transition(
81 | trigger="QueryToken",
82 | source="获取Token",
83 | dest="获取Token",
84 | conditions=lambda: self.queryTokenResult == 2,
85 | )
86 |
87 | # True-成功, False-失败
88 | self.machine.add_transition(
89 | trigger="RiskProcess",
90 | source="验证码",
91 | dest="获取Token",
92 | conditions=lambda: self.riskProcessResult is True,
93 | )
94 | self.machine.add_transition(
95 | trigger="RiskProcess",
96 | source="验证码",
97 | dest="验证码",
98 | conditions=lambda: self.riskProcessResult is False,
99 | )
100 |
101 | # True-成功, False-失败
102 | self.machine.add_transition(
103 | trigger="QueryTicket",
104 | source="等待余票",
105 | dest="创建订单",
106 | conditions=lambda: self.queryTicketResult is True,
107 | )
108 | self.machine.add_transition(
109 | trigger="QueryTicket",
110 | source="等待余票",
111 | dest="等待余票",
112 | conditions=lambda: self.queryTicketResult is False,
113 | )
114 |
115 | # 0-成功, 1-刷新, 2-等待, 3-失败
116 | self.machine.add_transition(
117 | trigger="CreateOrder",
118 | source="创建订单",
119 | dest="创建订单状态",
120 | conditions=lambda: self.createOrderResult == 0,
121 | )
122 | self.machine.add_transition(
123 | trigger="CreateOrder",
124 | source="创建订单",
125 | dest="获取Token",
126 | conditions=lambda: self.createOrderResult == 1,
127 | )
128 | self.machine.add_transition(
129 | trigger="CreateOrder",
130 | source="创建订单",
131 | dest="等待余票",
132 | conditions=lambda: self.createOrderResult == 2,
133 | )
134 | self.machine.add_transition(
135 | trigger="CreateOrder",
136 | source="创建订单",
137 | dest="创建订单",
138 | conditions=lambda: self.createOrderResult == 3,
139 | )
140 |
141 | # True-成功, False-失败
142 | self.machine.add_transition(
143 | trigger="CreateStatus",
144 | source="创建订单状态",
145 | dest="完成",
146 | conditions=lambda: self.createStatusResult is True,
147 | )
148 | self.machine.add_transition(
149 | trigger="CreateStatus",
150 | source="创建订单状态",
151 | dest="创建订单",
152 | conditions=lambda: self.createStatusResult is False,
153 | )
154 |
155 | # 关闭Transitions自带日志
156 | logging.getLogger("transitions").setLevel(logging.CRITICAL)
157 |
158 | @logger.catch
159 | def QueryTokenAction(self) -> None:
160 | """
161 | 获取Token
162 |
163 | 返回值: 0-成功, 1-验证码, 2-失败
164 | """
165 | self.queryTokenResult = self.api.QueryToken()
166 | # 顺路
167 | self.api.QueryAmount()
168 |
169 | @logger.catch
170 | def RiskProcessAction(self) -> None:
171 | """
172 | 验证码
173 |
174 | 返回值: True-成功, False-失败
175 | """
176 | # 获取流水成功
177 | if self.api.RiskInfo():
178 | challenge = self.api.GetRiskChallenge()
179 | validate = self.cap.Geetest(challenge)
180 | self.riskProcessResult = self.api.RiskValidate(validate)
181 | else:
182 | self.riskProcessResult = False
183 |
184 | @logger.catch
185 | def QueryTicketAction(self) -> None:
186 | """
187 | 等待余票
188 |
189 | 返回值: True-成功, False-失败
190 | """
191 | self.queryTicketResult = self.api.QueryAmount()
192 |
193 | @logger.catch
194 | def CreateOrderAction(self) -> None:
195 | """
196 | 创建订单
197 |
198 | 返回值: 0-成功, 1-刷新, 2-等待, 3-失败
199 | """
200 | self.createOrderResult = self.api.CreateOrder()
201 |
202 | @logger.catch
203 | def CreateStatusAction(self) -> None:
204 | """
205 | 创建订单状态
206 |
207 | 返回值: True-成功, False-失败
208 | """
209 | self.createStatusResult = self.api.GetOrderStatus() if self.api.CreateOrderStatus() else False
210 |
211 | @logger.catch
212 | def DrawFSM(self) -> None:
213 | """
214 | 状态机图输出
215 | """
216 | self.machine.get_graph().draw("./assest/fsm.png", prog="dot")
217 |
218 | @logger.catch
219 | def Run(self) -> bool:
220 | """
221 | 任务流
222 | """
223 | job = {
224 | "开始": "Next",
225 | "获取Token": "QueryToken",
226 | "验证码": "RiskProcess",
227 | "等待余票": "QueryTicket",
228 | "创建订单": "CreateOrder",
229 | "创建订单状态": "CreateStatus",
230 | }
231 | while self.state != "完成": # type: ignore
232 | sleep(self.sleep)
233 | if self.state in job.keys(): # type: ignore
234 | self.trigger(job[self.state]) # type: ignore
235 | else:
236 | raise
237 | return True
238 |
--------------------------------------------------------------------------------
/util/Data/__init__.py:
--------------------------------------------------------------------------------
1 | import base64
2 | import datetime
3 | import json
4 | from sys import exit
5 |
6 | import inquirer
7 | import machineid
8 | import pytz
9 | from Crypto.Cipher import AES
10 | from Crypto.Util.Padding import pad, unpad
11 | from inquirer.themes import GreenPassion
12 | from loguru import logger
13 | from qrcode import QRCode # type: ignore
14 |
15 |
16 | class Data:
17 | """
18 | 数据处理
19 | """
20 |
21 | @logger.catch
22 | def JsonpToDict(self, data: str) -> dict:
23 | """
24 | JSONP转JSON
25 |
26 | data: 待转换数据
27 | """
28 | startIndex = data.find("(") + 1
29 | endIndex = data.rfind(")")
30 | json_str = data[startIndex:endIndex]
31 | return json.loads(json_str)
32 |
33 | @logger.catch
34 | def QRGenerate(self, url: str, img_path: str) -> None:
35 | """
36 | 生成二维码
37 |
38 | url: 链接
39 | img_path: 保存路径
40 | """
41 | qr = QRCode()
42 | qr.add_data(url)
43 | qr.print_ascii(invert=True)
44 |
45 | img = qr.make_image()
46 | img.save(img_path)
47 |
48 | @logger.catch
49 | def SeleniumCookieFormat(self, cookie: list) -> dict:
50 | """
51 | 将Selenium输出的Cookie转为标准Cookie
52 |
53 | cookie: Selenium输出的Cookie
54 | """
55 | dist = {}
56 | for i in cookie:
57 | dist[i["name"]] = i["value"]
58 | return dist
59 |
60 | @logger.catch
61 | def StrCookieFormat(self, cookie: str) -> dict:
62 | """
63 | 将字符串Cookie转为标准Cookie
64 |
65 | cookie: 字符串Cookie
66 | """
67 | dist = {}
68 | cookies = cookie.split("; ")
69 | for cookie in cookies:
70 | if "=" in cookie:
71 | key, value = cookie.split("=", 1)
72 | dist[key] = value
73 | return dist
74 |
75 | @logger.catch
76 | def TimestampFormat(
77 | self,
78 | timestamp: int,
79 | format_type: str = "s",
80 | countdown: bool = False,
81 | ) -> str:
82 | """
83 | 时间戳转换
84 |
85 | timestamp: 时间戳
86 | format_type: 精确到 s: 秒 m: 分 d: 天
87 | countdown: 是否为 倒计时时间戳
88 | """
89 | if countdown:
90 | if timestamp > 0:
91 | countdown_delta = datetime.timedelta(seconds=timestamp)
92 | days = countdown_delta.days
93 | hours, remainder = divmod(countdown_delta.seconds, 3600)
94 | minutes, seconds = divmod(remainder, 60)
95 | return f"{days}天{hours}时{minutes}分{seconds}秒"
96 | else:
97 | return ""
98 |
99 | else:
100 | CST = pytz.timezone("Asia/Shanghai")
101 | formatted_time = datetime.datetime.fromtimestamp(timestamp, tz=CST)
102 | match format_type:
103 | case "s":
104 | return formatted_time.strftime("%Y-%m-%d %H:%M:%S")
105 | case "m":
106 | return formatted_time.strftime("%Y-%m-%d %H:%M")
107 | case "d":
108 | return formatted_time.strftime("%Y-%m-%d")
109 | case _:
110 | raise
111 |
112 | @logger.catch
113 | def TimestampCheck(self, timestamp: int, duration: int = 15) -> bool:
114 | """
115 | 时间戳有效性检查
116 |
117 | timestamp: 开始时间戳
118 | duration: 持续时间 分钟
119 | """
120 | timestamp_now = datetime.datetime.now().timestamp()
121 | if timestamp + duration * 60 >= timestamp_now >= timestamp:
122 | return True
123 | else:
124 | return False
125 |
126 | @logger.catch
127 | def PasswordRSAEncrypt(self, password: str, public_key: str) -> str:
128 | """
129 | RSA加密密码
130 |
131 | password: 密码
132 | public_key: 公钥
133 | """
134 | from Crypto.Cipher import PKCS1_v1_5
135 | from Crypto.PublicKey import RSA
136 |
137 | key = RSA.import_key(public_key)
138 | cipher = PKCS1_v1_5.new(key)
139 | cipher_text = cipher.encrypt(password.encode())
140 |
141 | return base64.b64encode(cipher_text).decode("utf-8")
142 |
143 | @logger.catch
144 | def AESEncrypt(self, data: str) -> str:
145 | """
146 | AES-128 加密
147 |
148 | data: 数据
149 | key: 硬件ID
150 | """
151 | key = machineid.id().encode()[:16]
152 | cipher = AES.new(key, AES.MODE_ECB)
153 | cipher_text = cipher.encrypt(pad(data.encode(), AES.block_size))
154 | return base64.b64encode(cipher_text).decode("utf-8")
155 |
156 | @logger.catch
157 | def AESDecrypt(self, data: str) -> str:
158 | """
159 | AES-128 解密
160 |
161 | data: 数据
162 | key: 硬件ID
163 | """
164 | key = machineid.id().encode()[:16]
165 | cipher = AES.new(key, AES.MODE_ECB)
166 | cipher_text = base64.b64decode(data.encode("utf-8"))
167 | try:
168 | decrypted_text = unpad(cipher.decrypt(cipher_text), AES.block_size)
169 | return decrypted_text.decode("utf-8")
170 | except Exception:
171 | logger.error("【解密】这是你的配置吗?")
172 | exit()
173 |
174 | @logger.catch
175 | def CookieAppend(self, baseCookie: dict) -> dict:
176 | """
177 | 补充非浏览器登录用户Cookie数据
178 |
179 | baseCookie: 基础Cookie
180 | """
181 | # 已知
182 | dist = {
183 | # https://blog.csdn.net/weixin_41489908/article/details/130643493
184 | "_uuid": "",
185 | # https://blog.csdn.net/weixin_41489908/article/details/130686625
186 | "b_lsid": "",
187 | # https://github.com/SocialSisterYi/bilibili-API-collect/issues/795#issuecomment-1805005704
188 | "b_nut": "",
189 | # https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/misc/sign/bili_ticket.md
190 | "bili_ticket": "",
191 | "bili_ticket_expires": "",
192 | # https://api.bilibili.com/x/frontend/finger/spi
193 | "buvid4": "",
194 | # https://github.com/SocialSisterYi/bilibili-API-collect/issues/933#issuecomment-1931506993
195 | "fingerprint": "",
196 | # TODO 浏览器JS
197 | "deviceFingerprint": "",
198 | }
199 | dist["buvid_fp"] = dist["fingerprint"]
200 | return dist | baseCookie
201 |
202 | @logger.catch
203 | def Inquire(
204 | self,
205 | type: str = "Text",
206 | message: str = "",
207 | choices: list | None = None,
208 | default: str | list | bool | None = None,
209 | ) -> str:
210 | """
211 | 交互
212 |
213 | type: 交互类型 Text, Confirm, List, Checkbox
214 | message: 提示信息
215 | default: 默认值 根据type决定类型
216 | choices: 选项
217 | """
218 | choiceMethod = ["List", "Checkbox"]
219 | method = {
220 | "Text": inquirer.Text,
221 | "Confirm": inquirer.Confirm,
222 | "List": inquirer.List,
223 | "Checkbox": inquirer.Checkbox,
224 | "Password": inquirer.Password,
225 | }
226 |
227 | process = method[type]
228 | res = inquirer.prompt(
229 | [process(name="res", message=message, default=default, **({"choices": choices} if type in choiceMethod else {}))],
230 | theme=GreenPassion(),
231 | )
232 |
233 | if res is not None:
234 | return res["res"]
235 | else:
236 | logger.error("【交互】未知错误!")
237 | exit()
238 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.toptal.com/developers/gitignore/api/qt,linux,direnv,dotenv,python,virtualenv,visualstudiocode,node
2 | # Edit at https://www.toptal.com/developers/gitignore?templates=qt,linux,direnv,dotenv,python,virtualenv,visualstudiocode,node
3 |
4 | ### direnv ###
5 | .direnv
6 | .envrc
7 |
8 | ### dotenv ###
9 | .env
10 |
11 | ### Linux ###
12 | *~
13 |
14 | # temporary files which can be created if a process still has a handle open of a deleted file
15 | .fuse_hidden*
16 |
17 | # KDE directory preferences
18 | .directory
19 |
20 | # Linux trash folder which might appear on any partition or disk
21 | .Trash-*
22 |
23 | # .nfs files are created when an open file is removed but is still being accessed
24 | .nfs*
25 |
26 | ### Node ###
27 | # Logs
28 | logs
29 | *.log
30 | npm-debug.log*
31 | yarn-debug.log*
32 | yarn-error.log*
33 | lerna-debug.log*
34 | .pnpm-debug.log*
35 |
36 | # Diagnostic reports (https://nodejs.org/api/report.html)
37 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
38 |
39 | # Runtime data
40 | pids
41 | *.pid
42 | *.seed
43 | *.pid.lock
44 |
45 | # Directory for instrumented libs generated by jscoverage/JSCover
46 | lib-cov
47 |
48 | # Coverage directory used by tools like istanbul
49 | coverage
50 | *.lcov
51 |
52 | # nyc test coverage
53 | .nyc_output
54 |
55 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
56 | .grunt
57 |
58 | # Bower dependency directory (https://bower.io/)
59 | bower_components
60 |
61 | # node-waf configuration
62 | .lock-wscript
63 |
64 | # Compiled binary addons (https://nodejs.org/api/addons.html)
65 | build/Release
66 |
67 | # Dependency directories
68 | node_modules/
69 | jspm_packages/
70 |
71 | # Snowpack dependency directory (https://snowpack.dev/)
72 | web_modules/
73 |
74 | # TypeScript cache
75 | *.tsbuildinfo
76 |
77 | # Optional npm cache directory
78 | .npm
79 |
80 | # Optional eslint cache
81 | .eslintcache
82 |
83 | # Optional stylelint cache
84 | .stylelintcache
85 |
86 | # Microbundle cache
87 | .rpt2_cache/
88 | .rts2_cache_cjs/
89 | .rts2_cache_es/
90 | .rts2_cache_umd/
91 |
92 | # Optional REPL history
93 | .node_repl_history
94 |
95 | # Output of 'npm pack'
96 | *.tgz
97 |
98 | # Yarn Integrity file
99 | .yarn-integrity
100 |
101 | # dotenv environment variable files
102 | .env.development.local
103 | .env.test.local
104 | .env.production.local
105 | .env.local
106 |
107 | # parcel-bundler cache (https://parceljs.org/)
108 | .cache
109 | .parcel-cache
110 |
111 | # Next.js build output
112 | .next
113 | out
114 |
115 | # Nuxt.js build / generate output
116 | .nuxt
117 | dist
118 |
119 | # Gatsby files
120 | .cache/
121 | # Comment in the public line in if your project uses Gatsby and not Next.js
122 | # https://nextjs.org/blog/next-9-1#public-directory-support
123 | # public
124 |
125 | # vuepress build output
126 | .vuepress/dist
127 |
128 | # vuepress v2.x temp and cache directory
129 | .temp
130 |
131 | # Docusaurus cache and generated files
132 | .docusaurus
133 |
134 | # Serverless directories
135 | .serverless/
136 |
137 | # FuseBox cache
138 | .fusebox/
139 |
140 | # DynamoDB Local files
141 | .dynamodb/
142 |
143 | # TernJS port file
144 | .tern-port
145 |
146 | # Stores VSCode versions used for testing VSCode extensions
147 | .vscode-test
148 |
149 | # yarn v2
150 | .yarn/cache
151 | .yarn/unplugged
152 | .yarn/build-state.yml
153 | .yarn/install-state.gz
154 | .pnp.*
155 |
156 | ### Node Patch ###
157 | # Serverless Webpack directories
158 | .webpack/
159 |
160 | # Optional stylelint cache
161 |
162 | # SvelteKit build / generate output
163 | .svelte-kit
164 |
165 | ### Python ###
166 | # Byte-compiled / optimized / DLL files
167 | __pycache__/
168 | *.py[cod]
169 | *$py.class
170 |
171 | # C extensions
172 | *.so
173 |
174 | # Distribution / packaging
175 | .Python
176 | build/
177 | develop-eggs/
178 | dist/
179 | downloads/
180 | eggs/
181 | .eggs/
182 | lib/
183 | lib64/
184 | parts/
185 | sdist/
186 | var/
187 | wheels/
188 | share/python-wheels/
189 | *.egg-info/
190 | .installed.cfg
191 | *.egg
192 | MANIFEST
193 |
194 | # PyInstaller
195 | # Usually these files are written by a python script from a template
196 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
197 | *.manifest
198 |
199 | # Installer logs
200 | pip-log.txt
201 | pip-delete-this-directory.txt
202 |
203 | # Unit test / coverage reports
204 | htmlcov/
205 | .tox/
206 | .nox/
207 | .coverage
208 | .coverage.*
209 | nosetests.xml
210 | coverage.xml
211 | *.cover
212 | *.py,cover
213 | .hypothesis/
214 | .pytest_cache/
215 | cover/
216 |
217 | # Translations
218 | *.mo
219 | *.pot
220 |
221 | # Django stuff:
222 | local_settings.py
223 | db.sqlite3
224 | db.sqlite3-journal
225 |
226 | # Flask stuff:
227 | instance/
228 | .webassets-cache
229 |
230 | # Scrapy stuff:
231 | .scrapy
232 |
233 | # Sphinx documentation
234 | docs/_build/
235 |
236 | # PyBuilder
237 | .pybuilder/
238 | target/
239 |
240 | # Jupyter Notebook
241 | .ipynb_checkpoints
242 |
243 | # IPython
244 | profile_default/
245 | ipython_config.py
246 |
247 | # pyenv
248 | # For a library or package, you might want to ignore these files since the code is
249 | # intended to run in multiple environments; otherwise, check them in:
250 | # .python-version
251 |
252 | # pipenv
253 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
254 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
255 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
256 | # install all needed dependencies.
257 | #Pipfile.lock
258 |
259 | # poetry
260 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
261 | # This is especially recommended for binary packages to ensure reproducibility, and is more
262 | # commonly ignored for libraries.
263 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
264 | #poetry.lock
265 |
266 | # pdm
267 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
268 | #pdm.lock
269 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
270 | # in version control.
271 | # https://pdm.fming.dev/#use-with-ide
272 | .pdm.toml
273 |
274 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
275 | __pypackages__/
276 |
277 | # Celery stuff
278 | celerybeat-schedule
279 | celerybeat.pid
280 |
281 | # SageMath parsed files
282 | *.sage.py
283 |
284 | # Environments
285 | .venv
286 | env/
287 | venv/
288 | ENV/
289 | env.bak/
290 | venv.bak/
291 |
292 | # Spyder project settings
293 | .spyderproject
294 | .spyproject
295 |
296 | # Rope project settings
297 | .ropeproject
298 |
299 | # mkdocs documentation
300 | /site
301 |
302 | # mypy
303 | .mypy_cache/
304 | .dmypy.json
305 | dmypy.json
306 |
307 | # Pyre type checker
308 | .pyre/
309 |
310 | # pytype static type analyzer
311 | .pytype/
312 |
313 | # Cython debug symbols
314 | cython_debug/
315 |
316 | # PyCharm
317 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
318 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
319 | # and can be added to the global gitignore or merged into this file. For a more nuclear
320 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
321 | #.idea/
322 |
323 | ### Python Patch ###
324 | # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
325 | poetry.toml
326 |
327 | # ruff
328 | .ruff_cache/
329 |
330 | # LSP config files
331 | pyrightconfig.json
332 |
333 | ### Qt ###
334 | # C++ objects and libs
335 | *.slo
336 | *.lo
337 | *.o
338 | *.a
339 | *.la
340 | *.lai
341 | *.so.*
342 | *.dll
343 | *.dylib
344 |
345 | # Qt-es
346 | object_script.*.Release
347 | object_script.*.Debug
348 | *_plugin_import.cpp
349 | /.qmake.cache
350 | /.qmake.stash
351 | *.pro.user
352 | *.pro.user.*
353 | *.qbs.user
354 | *.qbs.user.*
355 | *.moc
356 | moc_*.cpp
357 | moc_*.h
358 | qrc_*.cpp
359 | ui_*.h
360 | *.qmlc
361 | *.jsc
362 | *build-*
363 | *.qm
364 | *.prl
365 |
366 | # Qt unit tests
367 | target_wrapper.*
368 |
369 | # QtCreator
370 | *.autosave
371 |
372 | # QtCreator Qml
373 | *.qmlproject.user
374 | *.qmlproject.user.*
375 |
376 | # QtCreator CMake
377 | CMakeLists.txt.user*
378 |
379 | # QtCreator 4.8< compilation database
380 | compile_commands.json
381 |
382 | # QtCreator local machine specific files for imported projects
383 | *creator.user*
384 |
385 | *_qmlcache.qrc
386 |
387 | ### VirtualEnv ###
388 | # Virtualenv
389 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/
390 | [Bb]in
391 | [Ii]nclude
392 | [Ll]ib
393 | [Ll]ib64
394 | [Ll]ocal
395 | [Ss]cripts
396 | pyvenv.cfg
397 | pip-selfcheck.json
398 |
399 | ### VisualStudioCode ###
400 | .vscode
401 |
402 | # Local History for Visual Studio Code
403 | .history/
404 |
405 | # Built Visual Studio Code Extensions
406 | *.vsix
407 |
408 | ### VisualStudioCode Patch ###
409 | # Ignore all local history of files
410 | .history
411 | .ionide
412 |
413 | # End of https://www.toptal.com/developers/gitignore/api/qt,linux,direnv,dotenv,python,virtualenv,visualstudiocode,node
414 |
415 | ### Sphinx
416 |
417 | doc/_build/
418 | doc/locale/
419 |
420 | ### Project
421 |
422 | *.jpg
423 | config/**/**.yaml
424 | upx
425 |
--------------------------------------------------------------------------------
/util/Bilibili/__init__.py:
--------------------------------------------------------------------------------
1 | import json
2 | import time
3 | import webbrowser
4 | from random import randint
5 | from sys import exit
6 |
7 | from loguru import logger
8 |
9 | from util.Data import Data
10 | from util.Request import Request
11 |
12 |
13 | class Bilibili:
14 | """
15 | 会员购
16 | """
17 |
18 | @logger.catch
19 | def __init__(
20 | self,
21 | net: Request,
22 | projectId: int,
23 | screenId: int,
24 | skuId: int,
25 | buyer: dict,
26 | orderType: int = 1,
27 | count: int = 1,
28 | ):
29 | """
30 | 初始化
31 |
32 | net: 网络实例
33 | projectId: 项目ID
34 | screenId: 场次ID
35 | skuId: 商品ID
36 | buyer: 购买者信息
37 | orderType: 订单类型
38 | count: 购买数量
39 | """
40 | self.net = net
41 |
42 | self.projectId = projectId
43 | self.screenId = screenId
44 | self.skuId = skuId
45 | self.buyer = buyer
46 |
47 | self.orderType = orderType
48 | self.count = count
49 |
50 | self.scene = "neul-next"
51 | self.screenPath = 0
52 | self.skuPath = 0
53 |
54 | self.data = Data()
55 | self.risked = False
56 | self.queryNotice = False
57 | self.createNotice = False
58 |
59 | @logger.catch
60 | def QueryToken(self) -> int:
61 | """
62 | 获取Token
63 |
64 | 返回: 0-成功, 1-风控, 2-未知
65 | """
66 | # 成功
67 | if not self.risked:
68 | url = f"https://show.bilibili.com/api/ticket/order/prepare?project_id={self.projectId}"
69 |
70 | # 刚刚验证完
71 | else:
72 | url = f"https://show.bilibili.com/api/ticket/order/prepare?project_id={self.projectId}&token={self.token}&gaia_vtoken={self.token}"
73 | self.risked = False
74 |
75 | params = {
76 | "project_id": self.projectId,
77 | "screen_id": self.screenId,
78 | "sku_id": self.skuId,
79 | "count": self.count,
80 | "order_type": self.orderType,
81 | "token": "",
82 | "requestSource": self.scene,
83 | "newRisk": True,
84 | }
85 | res = self.net.Response(method="post", url=url, params=params).json()
86 | data = res["data"]
87 | code = res["errno"]
88 |
89 | # 成功
90 | if code == 0:
91 | logger.info("【获取Token】Token获取成功!")
92 | self.token = data["token"]
93 | return 0
94 |
95 | # 风控
96 | elif code == -401:
97 | riskParams = data["ga_data"]["riskParams"]
98 | self.mid = riskParams["mid"]
99 | self.decisionType = riskParams["decision_type"]
100 | self.buvid = riskParams["buvid"]
101 | self.ip = riskParams["ip"]
102 | self.scene = riskParams["scene"]
103 | self.ua = riskParams["ua"]
104 | self.voucher = riskParams["v_voucher"]
105 | logger.warning("【获取Token】已风控")
106 | return 1
107 |
108 | # projectID/ScreenId/SkuID错误
109 | if code in [100080, 100082]:
110 | logger.error("【获取Token】项目/场次/价位不存在!")
111 | exit()
112 |
113 | # 没开票
114 | if code == 100041:
115 | logger.error("【获取Token】该项目暂未开票!")
116 | exit()
117 |
118 | # 停售
119 | if code == 100039:
120 | logger.error("【获取Token】早停售了你抢牛魔呢")
121 | exit()
122 |
123 | # 未知
124 | else:
125 | logger.error(f"【获取Token】{code}: {res['msg']}")
126 | return 2
127 |
128 | @logger.catch
129 | def QueryAmount(self) -> bool:
130 | """
131 | 获取票数
132 |
133 | 返回: True-有票, False-无票
134 | """
135 | url = f"https://show.bilibili.com/api/ticket/project/getV2?version=134&id={self.projectId}&project_id={self.projectId}&requestSource={self.scene}"
136 | res = self.net.Response(method="get", url=url).json()
137 | data = res["data"]
138 | code = res["errno"]
139 |
140 | # 成功
141 | if code == 0:
142 | # 有保存Sku位置
143 | if data["screen_list"][self.screenPath]["ticket_list"][self.skuPath]["id"] == self.skuId:
144 | self.cost = data["screen_list"][self.screenPath]["ticket_list"][self.skuPath]["price"]
145 | self.saleStart = data["screen_list"][self.screenPath]["ticket_list"][self.skuPath]["saleStart"]
146 | clickable = data["screen_list"][self.screenPath]["ticket_list"][self.skuPath]["clickable"]
147 |
148 | # 没保存Sku位置
149 | else:
150 | for i, screen in enumerate(data["screen_list"]):
151 | if screen["id"] == self.screenId:
152 | for j, sku in enumerate(screen["ticket_list"]):
153 | if sku["id"] == self.skuId:
154 | self.cost = sku["price"]
155 | self.saleStart = sku["saleStart"]
156 | clickable = sku["clickable"]
157 | self.screenPath = i
158 | self.skuPath = j
159 | break
160 |
161 | # 有票
162 | if clickable:
163 | logger.info("【获取票数】当前可购买")
164 | return True
165 |
166 | # 无票
167 | else:
168 | if not self.queryNotice:
169 | logger.warning("【获取票数】当前无票, 系统正在循环蹲票中! 请稍后")
170 | self.queryNotice = True
171 | return False
172 |
173 | # 失败
174 | else:
175 | logger.error(f"【获取票数】{code}: {res['msg']}")
176 | return False
177 |
178 | @logger.catch
179 | def RiskInfo(self) -> bool:
180 | """
181 | 获取流水
182 |
183 | 返回: True-成功, False-失败
184 | """
185 | # 获取CSRF
186 | if self.csrf is None:
187 | self.csrf = self.net.GetCookie()["bili_jct"]
188 |
189 | url = "https://api.bilibili.com/x/gaia-vgate/v1/register"
190 | params = {
191 | "buvid": self.buvid,
192 | "csrf": self.csrf,
193 | "decision_type": self.decisionType,
194 | "ip": self.ip,
195 | "mid": self.mid,
196 | "origin_scene": self.scene,
197 | "scene": self.scene,
198 | "ua": self.ua,
199 | "v_voucher": self.voucher,
200 | }
201 | res = self.net.Response(method="post", url=url, params=params).json()
202 | data = res["data"]
203 | code = res["code"]
204 |
205 | # 成功
206 | if code == 0:
207 | self.token = data["token"]
208 | self.challenge = data["geetest"]["challenge"]
209 | self.gt = data["geetest"]["gt"]
210 | return True
211 |
212 | # 失败
213 | else:
214 | logger.error(f"【获取流水】{code}: {res['message']}")
215 | return False
216 |
217 | @logger.catch
218 | def GetRiskChallenge(self) -> str:
219 | """
220 | 获取流水号
221 | """
222 | return self.challenge
223 |
224 | @logger.catch
225 | def RiskValidate(self, validate: str) -> bool:
226 | """
227 | 校验
228 |
229 | validate: 校验值
230 |
231 | 返回值: True-成功, False-失败
232 | """
233 | url = "https://api.bilibili.com/x/gaia-vgate/v1/validate"
234 | params = {
235 | "challenge": self.challenge,
236 | "csrf": self.csrf,
237 | "seccode": validate + "|jordan",
238 | "token": self.token,
239 | "validate": validate,
240 | }
241 | res = self.net.Response(method="get", url=url, params=params).json()
242 | code = res["code"]
243 |
244 | # 成功&有效
245 | if code == 0 and res["data"]["is_valid"] == 1:
246 | self.risked = True
247 | cookie = self.net.GetCookie()
248 | cookie["x-bili-gaia-vtoken"] = self.token
249 | self.net.RefreshCookie(cookie)
250 | return True
251 |
252 | # 失败
253 | else:
254 | logger.error(f"【校验】{code}: {res['message']}")
255 | return False
256 |
257 | @logger.catch
258 | def CreateOrder(self) -> int:
259 | """
260 | 创建订单
261 |
262 | 返回: 0-成功, 1-Token过期, 2-库存不足, 3-失败
263 | """
264 | url = f"https://show.bilibili.com/api/ticket/order/createV2?project_id={self.projectId}"
265 | timestamp = int(round(time.time() * 1000))
266 | clickPosition = {
267 | "x": randint(1300, 1500),
268 | "y": randint(20, 100),
269 | "origin": timestamp - randint(2500, 10000),
270 | "now": timestamp,
271 | }
272 | params = {
273 | "project_id": self.projectId,
274 | "screen_id": self.screenId,
275 | "sku_id": self.skuId,
276 | "count": self.count,
277 | "pay_money": self.cost * self.count,
278 | "order_type": self.orderType,
279 | "timestamp": timestamp,
280 | "buyer_info": f"[{json.dumps(self.buyer)}]",
281 | "token": self.token,
282 | "deviceId": "",
283 | "clickPosition": json.dumps(clickPosition),
284 | "newRisk": True,
285 | "requestSource": self.scene,
286 | }
287 | res = self.net.Response(method="post", url=url, params=params).json()
288 | data = res["data"]
289 | code = res["errno"]
290 |
291 | # 成功
292 | if code == 0:
293 | self.orderId = data["orderId"]
294 | self.orderToken = data["token"]
295 | logger.info("【创建订单】订单创建成功!")
296 | return 0
297 |
298 | # Token过期
299 | elif "10005" in str(code):
300 | logger.warning("【创建订单】Token过期! 即将重新获取")
301 | return 1
302 |
303 | # 库存不足 219,100009
304 | elif code in [219, 100009]:
305 | if self.data.TimestampCheck(timestamp=self.saleStart, duration=15):
306 | if self.createNotice:
307 | logger.warning("【创建订单】目前处于开票15分钟黄金期, 已为您忽略无票提示!")
308 | self.createNotice = True
309 | return 3
310 | else:
311 | logger.warning("【创建订单】库存不足!")
312 | return 2
313 |
314 | # 存在未付款订单
315 | elif code == 100079:
316 | logger.error("【创建订单】存在未付款订单! 请在支付或取消订单后再次运行")
317 | exit()
318 |
319 | # 订单已存在/已购买
320 | elif code == 100049:
321 | logger.error("【创建订单】该项目每人限购1张, 已存在购买订单")
322 | exit()
323 |
324 | # 本项目需要联系人信息
325 | elif code == 209001:
326 | logger.error("【创建订单】目前仅支持实名制一人一票类活动哦~(其他类型活动也用不着上脚本吧啊喂)")
327 | exit()
328 |
329 | # 失败
330 | else:
331 | if not self.createNotice:
332 | logger.error(f"【创建订单】{code}: {res['msg']}")
333 | return 3
334 |
335 | @logger.catch
336 | def CreateOrderStatus(self) -> bool:
337 | """
338 | 创建订单状态
339 |
340 | 返回: True-成功, False-失败
341 | """
342 | url = f"https://show.bilibili.com/api/ticket/order/createstatus?token={self.orderToken}&project_id={self.projectId}&orderId={self.orderId}"
343 | res = self.net.Response(method="get", url=url).json()
344 | code = res["errno"]
345 |
346 | # 成功
347 | if code == 0:
348 | logger.info("【创建订单状态】锁单成功!")
349 | return True
350 |
351 | # 失败
352 | else:
353 | logger.error(f"【创建订单状态】{code}: {res['msg']}")
354 | return False
355 |
356 | @logger.catch
357 | def GetOrderStatus(self) -> bool:
358 | """
359 | 获取订单状态
360 |
361 | 返回: True-成功, False-失败
362 | """
363 | url = f"https://show.bilibili.com/api/ticket/order/info?order_id={self.orderId}"
364 | res = self.net.Response(method="get", url=url).json()
365 | code = res["errno"]
366 |
367 | # 成功
368 | if code == 0:
369 | logger.info("【获取订单状态】请扫码/在打开的浏览器页面进行支付!")
370 | webbrowser.open(f"https://show.bilibili.com/platform/orderDetail.html?order_id={self.orderId}")
371 | return True
372 |
373 | # 失败
374 | else:
375 | logger.error(f"【获取订单状态】{code}: {res['msg']}")
376 | return False
377 |
--------------------------------------------------------------------------------
/util/Login/__init__.py:
--------------------------------------------------------------------------------
1 | import json
2 | import re
3 | import time
4 | from sys import exit
5 |
6 | import browsers
7 | from bili_ticket_gt_python import ClickPy, SlidePy
8 | from loguru import logger
9 | from selenium import webdriver
10 | from selenium.webdriver.common.by import By
11 | from selenium.webdriver.support import expected_conditions as EC
12 | from selenium.webdriver.support.ui import WebDriverWait
13 |
14 | from util import Captcha, Data, Request
15 |
16 |
17 | class Login:
18 | """
19 | 账号登录
20 |
21 | 文档: https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/login/login_action
22 | """
23 |
24 | @logger.catch
25 | def __init__(
26 | self,
27 | net: Request,
28 | checkStatus: bool = True,
29 | ):
30 | """
31 | 初始化
32 |
33 | net: 网络实例
34 | isCheckStatus: 是否检查登录状态
35 | """
36 | self.net = net
37 | self.isCheckStatus = checkStatus
38 |
39 | self.cookie = {}
40 | self.data = Data()
41 | self.click = ClickPy()
42 | self.slide = SlidePy()
43 |
44 | self.source = "main_web"
45 |
46 | @logger.catch
47 | def QRCode(self) -> dict:
48 | """
49 | 扫码登录
50 |
51 | 文档: https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/login/login_action/QR.md
52 | """
53 | self.net.Response(method="get", url="https://www.bilibili.com/")
54 |
55 | resp = self.net.Response(
56 | method="get",
57 | url="https://passport.bilibili.com/x/passport-login/web/qrcode/generate",
58 | ).json()
59 |
60 | if resp["code"] == 0:
61 | url = resp["data"]["url"]
62 | self.data.QRGenerate(url, "qr.jpg")
63 |
64 | while True:
65 | time.sleep(1.5)
66 | url = "https://passport.bilibili.com/x/passport-login/web/qrcode/poll?source=main-fe-header&qrcode_key=" + resp["data"]["qrcode_key"]
67 | respQR = self.net.Response(method="get", url=url).json()
68 |
69 | check = respQR["data"]
70 | if check["code"] == 0:
71 | logger.info("【登录】登录成功")
72 | self.cookie = self.net.GetCookie()
73 | # 补充Cookie参数
74 | # self.cookie = Data().CookieAppend(self.cookie) | self.cookie
75 | return self.Status()
76 |
77 | # 未扫描:86101 扫描未确认:86090
78 | elif check["code"] not in (86101, 86090):
79 | logger.error(f"【登录】{check['code']}: {check['message']}")
80 | exit()
81 |
82 | else:
83 | logger.error(f"【登录】服务器不知道送来啥东西{json.dumps(resp, indent=4)}")
84 | exit()
85 |
86 | @logger.catch
87 | def Selenium(self) -> dict:
88 | """
89 | Selenium登录
90 |
91 | Chrome WebDriver: https://googlechromelabs.github.io/chrome-for-testing/#stable
92 | """
93 | browser_list = [i for i in list(browsers.browsers()) if i["browser_type"] != "msie"]
94 |
95 | if browser_list:
96 | selenium_drivers = {
97 | "chrome": webdriver.Chrome,
98 | "firefox": webdriver.Firefox,
99 | "msedge": webdriver.Edge,
100 | "safari": webdriver.Safari,
101 | }
102 |
103 | for browser in browser_list:
104 | browser_type = browser["browser_type"]
105 | print(f"请在打开的 {browser_type} 浏览器中进行登录")
106 | driver = selenium_drivers[browser_type]()
107 |
108 | if driver:
109 | driver.maximize_window()
110 |
111 | try:
112 | driver.get("https://show.bilibili.com/")
113 | wait = WebDriverWait(driver, 30)
114 | event = wait.until(EC.element_to_be_clickable((By.CLASS_NAME, "nav-header-register")))
115 | driver.execute_script("arguments[0].click();", event)
116 | break
117 |
118 | except Exception as e:
119 | logger.exception(f"【登录】{e}")
120 | driver.quit()
121 |
122 | else:
123 | logger.error("【登录】所有浏览器/WebDriver尝试登录均失败")
124 | else:
125 | logger.error("【登录】未找到可用浏览器/WebDriver! 建议选择其他方式登录")
126 | exit()
127 |
128 | while True:
129 | time.sleep(0.5)
130 | if driver.page_source is None or "登录" not in driver.page_source:
131 | break
132 |
133 | logger.info("【登录】登录成功")
134 | driver.get("https://account.bilibili.com/account/home")
135 | seleniumCookie = driver.get_cookies()
136 | logger.info("【登录】Cookie已保存")
137 | self.cookie = self.data.SeleniumCookieFormat(seleniumCookie)
138 | driver.quit()
139 | return self.Status()
140 |
141 | @logger.catch
142 | def GetCaptcha(self) -> tuple:
143 | """
144 | 获取Captcha验证码并通过Geetest验证
145 |
146 | 文档: https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/login/login_action/readme.md
147 | """
148 | resp = self.net.Response(
149 | method="get",
150 | url="https://passport.bilibili.com/x/passport-login/captcha?source=main_web",
151 | ).json()
152 |
153 | if resp["code"] == 0:
154 | token = resp["data"]["token"]
155 | challenge = resp["data"]["geetest"]["challenge"]
156 | validate = Captcha(verify=self.click).Geetest(challenge)
157 | seccode = validate + "|jordan"
158 | return token, challenge, validate, seccode
159 | else:
160 | raise
161 |
162 | @logger.catch
163 | def GetPreCaptcha(self) -> tuple:
164 | """
165 | 获取PreCaptcha验证码并通过Geetest验证
166 |
167 | 文档: https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/login/login_action/readme.md
168 | """
169 | resp = self.net.Response(
170 | method="post",
171 | url="https://passport.bilibili.com/x/safecenter/captcha/pre",
172 | ).json()
173 |
174 | if resp["code"] == 0:
175 | token = resp["data"]["recaptcha_token"]
176 | challenge = resp["data"]["gee_challenge"]
177 | validate = Captcha(verify=self.click).Geetest(challenge)
178 | seccode = validate + "|jordan"
179 | return token, challenge, validate, seccode
180 | else:
181 | raise
182 |
183 | @logger.catch
184 | def Password(self, username: str, password: str) -> dict:
185 | """
186 | 账号密码登录
187 |
188 | username: 用户名
189 | password: 密码
190 |
191 | 文档: https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/login/login_action/password.md
192 | """
193 | token, challenge, validate, seccode = self.GetCaptcha()
194 |
195 | salt = self.net.Response(
196 | method="get",
197 | url="https://passport.bilibili.com/x/passport-login/web/key",
198 | ).json()
199 |
200 | salt_hash = salt["data"]["hash"]
201 | salt_key = salt["data"]["key"]
202 |
203 | params = {
204 | "username": username,
205 | "password": self.data.PasswordRSAEncrypt(salt_hash + password, salt_key),
206 | "keep": "0",
207 | "token": token,
208 | "challenge": challenge,
209 | "validate": validate,
210 | "seccode": seccode,
211 | "source": self.source,
212 | }
213 |
214 | resp = self.net.Response(
215 | method="post",
216 | url="https://passport.bilibili.com/x/passport-login/web/login",
217 | params=params,
218 | ).json()
219 |
220 | if resp["code"] == 0:
221 |
222 | if resp["data"]["status"] == 0:
223 | logger.info("【登录】登录成功")
224 | self.cookie = self.net.GetCookie()
225 | return self.Status()
226 |
227 | else: # 二次短信验证登录
228 | logger.info("【登录】登录失败, 需要二次验证")
229 |
230 | resp_url = resp["data"]["url"]
231 | tmp_token_match = re.search(r"tmp_token=(\w{32})", resp_url)
232 | tmp_token = tmp_token_match.group(1) if tmp_token_match else ""
233 | scene_match = re.search(r"scene=([^&]+)", resp_url)
234 | scene = scene_match.group(1) if scene_match else "loginTelCheck"
235 |
236 | info = self.net.Response(
237 | method="get",
238 | url=f"https://passport.bilibili.com/x/safecenter/user/info?tmp_code={tmp_token}",
239 | ).json()
240 |
241 | if info["data"]["account_info"]["bind_tel"]:
242 | hide_tel = info["data"]["account_info"]["hide_tel"]
243 | logger.info(f"【登录】手机号已绑定, 即将给 {hide_tel} 发送验证码")
244 |
245 | token, challenge, validate, seccode = self.GetPreCaptcha()
246 |
247 | resend_params = {
248 | "tmp_code": tmp_token,
249 | "sms_type": scene,
250 | "recaptcha_token": token,
251 | "gee_challenge": challenge,
252 | "gee_validate": validate,
253 | "gee_seccode": seccode,
254 | }
255 |
256 | resend = self.net.Response(
257 | method="post",
258 | url="https://passport.bilibili.com/x/safecenter/common/sms/send",
259 | params=resend_params,
260 | ).json()
261 |
262 | if resend["code"] != 0:
263 | logger.error(f"【登录】验证码发送失败: {resend['code']} {resend['message']}")
264 | exit()
265 |
266 | logger.info("【登录】验证码发送成功")
267 | resend_token = resend["data"]["captcha_key"]
268 | verify_code = self.data.Inquire(type="Text", message="请输入验证码")
269 |
270 | if resp["data"]["status"] == 1:
271 | data = {
272 | "verify_type": "sms",
273 | "tmp_code": tmp_token,
274 | "captcha_key": resend_token,
275 | "code": verify_code,
276 | }
277 | url = "https://passport.bilibili.com/x/safecenter/sec/verify"
278 |
279 | elif resp["data"]["status"] == 2:
280 | data = {
281 | "type": "loginTelCheck",
282 | "tmp_code": tmp_token,
283 | "captcha_key": resend_token,
284 | "code": verify_code,
285 | }
286 | url = "https://passport.bilibili.com/x/safecenter/login/tel/verify"
287 |
288 | else:
289 | logger.error(f"【登录】未知错误: {resp['data']['status']}")
290 |
291 | reverify = self.net.Response(method="post", url=url, params=data).json()
292 |
293 | if reverify["code"] != 0:
294 | logger.error(f"【登录】验证码登录失败: {reverify['code']} {reverify['message']}")
295 | exit()
296 | else:
297 | logger.info("【登录】验证码登录成功")
298 | self.net.Response(
299 | method="post",
300 | url="https://passport.bilibili.com/x/passport-login/web/exchange_cookie",
301 | params={"source": "risk", "code": reverify["data"]["code"]},
302 | ).json()
303 | self.cookie = self.net.GetCookie()
304 | return self.Status()
305 | else:
306 | logger.info("【登录】手机号未绑定, 请重新选择登录方式")
307 | exit()
308 | else:
309 | match int(resp["code"]):
310 | case -105:
311 | logger.error("【登录】验证码错误")
312 | case -400:
313 | logger.error("【登录】请求错误")
314 | case -629:
315 | logger.error("【登录】账号或密码错误")
316 | case -653:
317 | logger.error("【登录】用户名或密码不能为空")
318 | case -662:
319 | logger.error("【登录】提交超时,请重新提交")
320 | case -2001:
321 | logger.error("【登录】缺少必要的参数")
322 | case -2100:
323 | logger.error("【登录】需验证手机号或邮箱")
324 | case 2400:
325 | logger.error("【登录】登录秘钥错误")
326 | case 2406:
327 | logger.error("【登录】验证极验服务出错")
328 | case 86000:
329 | logger.error("【登录】RSA解密失败")
330 | case _:
331 | logger.error(f"【发送验证码】{resp['code']} {resp['message']}")
332 | exit()
333 |
334 | @logger.catch
335 | def SMSSend(self, tel: str) -> str:
336 | """
337 | 手机号登录 - 发送验证码
338 |
339 | tel: 手机号
340 | 返回: captcha_key
341 |
342 | 文档: https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/login/login_action/SMS.md
343 | """
344 | token, challenge, validate, seccode = self.GetCaptcha()
345 |
346 | params = {
347 | "cid": "86",
348 | "tel": tel,
349 | "source": self.source,
350 | "token": token,
351 | "challenge": challenge,
352 | "validate": validate,
353 | "seccode": seccode,
354 | }
355 |
356 | resp = self.net.Response(
357 | method="post",
358 | url="https://passport.bilibili.com/x/passport-login/web/sms/send",
359 | params=params,
360 | ).json()
361 |
362 | if resp["code"] == 0:
363 | logger.info("【登录】验证码发送成功")
364 | captcha_key = resp["data"]["captcha_key"]
365 | return captcha_key
366 | else:
367 | match int(resp["code"]):
368 | case -400:
369 | logger.error("【发送验证码】请求错误")
370 | case 1002:
371 | logger.error("【发送验证码】手机号码格式错误")
372 | case 1003:
373 | logger.error("【发送验证码】验证码已经发送")
374 | case 86203:
375 | logger.error("【发送验证码】短信发送次数已达上限")
376 | case 2406:
377 | logger.error("【发送验证码】验证极验服务出错")
378 | case 2400:
379 | logger.error("【发送验证码】登录秘钥错误")
380 | case _:
381 | logger.error(f"【发送验证码】{resp['code']} {resp['message']}")
382 | return ""
383 |
384 | @logger.catch
385 | def SMSVerify(self, tel: str, code: str, captcha_key: str) -> dict:
386 | """
387 | 手机号登录 - 发送验证码
388 |
389 | tel: 手机号
390 | int: 验证码
391 | captcha_key: 验证token
392 |
393 | 文档: https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/login/login_action/SMS.md
394 | """
395 | params = {
396 | "cid": "86",
397 | "tel": tel,
398 | "code": code,
399 | "source": self.source,
400 | "captcha_key": captcha_key,
401 | "keep": False,
402 | }
403 |
404 | resp = self.net.Response(
405 | method="post",
406 | url="https://passport.bilibili.com/x/passport-login/web/login/sms",
407 | params=params,
408 | ).json()
409 |
410 | if resp["code"] == 0:
411 | logger.info("【登录】登录成功")
412 | else:
413 | match int(resp["code"]):
414 | case 1006:
415 | logger.error("【登录】请输入正确的短信验证码")
416 | case 1007:
417 | logger.error("【登录】短信验证码已过期")
418 | case -400:
419 | logger.error("【登录】请求错误")
420 | case _:
421 | logger.error(f"【发送验证码】{resp['code']} {resp['message']}")
422 | raise Exception("手机号验证码登录失败")
423 |
424 | self.cookie = self.net.GetCookie()
425 | return self.Status()
426 |
427 | @logger.catch
428 | def Cookie(self, cookie: str) -> dict:
429 | """
430 | Cookie登录
431 |
432 | cookie: Cookie字符串
433 | """
434 | self.cookie = self.data.StrCookieFormat(cookie)
435 | return self.Status()
436 |
437 | @logger.catch
438 | def Status(self) -> dict:
439 | """
440 | 登录状态
441 |
442 | 文档: https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/login/login_info.md
443 | """
444 | self.net.RefreshCookie(self.cookie)
445 |
446 | if self.isCheckStatus:
447 | user = self.net.Response(method="get", url="https://api.bilibili.com/x/web-interface/nav").json()
448 |
449 | if user["data"]["isLogin"]:
450 | return self.cookie
451 | else:
452 | logger.error("【登录状态检测】登录失败")
453 | exit()
454 |
455 | else:
456 | logger.info("【登录状态检测】已关闭")
457 | return self.cookie
458 |
459 | @logger.catch
460 | def RefreshToken(self) -> bool:
461 | """
462 | 刷新Token
463 |
464 | https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/login/cookie_refresh.md
465 | """
466 | url = ""
467 | params = {}
468 | resp = self.net.Response(method="post", url=url, params=params).json()
469 |
470 | if resp["code"] == 0:
471 | logger.info("【刷新Token】刷新成功")
472 | return True
473 | else:
474 | logger.error("【刷新Token】刷新失败")
475 | return False
476 |
477 | @logger.catch
478 | def ExitLogin(self) -> bool:
479 | """
480 | 退出登录
481 | """
482 | resp = self.net.Response(
483 | method="get",
484 | url="https://passport.bilibili.com/login/exit/v2",
485 | params={"biliCSRF": self.cookie["bili_jct"]},
486 | ).json()
487 |
488 | if resp["code"] == 0 and resp["status"]:
489 | logger.info("【退出登录】注销Cookie成功")
490 | return True
491 | elif resp["code"] == 2202:
492 | logger.error("【退出登录】CSRF请求非法")
493 | return False
494 | else:
495 | logger.error("【退出登录】发生了什么")
496 | return False
497 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU General Public License is a free, copyleft license for
11 | software and other kinds of works.
12 |
13 | The licenses for most software and other practical works are designed
14 | to take away your freedom to share and change the works. By contrast,
15 | the GNU General Public License is intended to guarantee your freedom to
16 | share and change all versions of a program--to make sure it remains free
17 | software for all its users. We, the Free Software Foundation, use the
18 | GNU General Public License for most of our software; it applies also to
19 | any other work released this way by its authors. You can apply it to
20 | your programs, too.
21 |
22 | When we speak of free software, we are referring to freedom, not
23 | price. Our General Public Licenses are designed to make sure that you
24 | have the freedom to distribute copies of free software (and charge for
25 | them if you wish), that you receive source code or can get it if you
26 | want it, that you can change the software or use pieces of it in new
27 | free programs, and that you know you can do these things.
28 |
29 | To protect your rights, we need to prevent others from denying you
30 | these rights or asking you to surrender the rights. Therefore, you have
31 | certain responsibilities if you distribute copies of the software, or if
32 | you modify it: responsibilities to respect the freedom of others.
33 |
34 | For example, if you distribute copies of such a program, whether
35 | gratis or for a fee, you must pass on to the recipients the same
36 | freedoms that you received. You must make sure that they, too, receive
37 | or can get the source code. And you must show them these terms so they
38 | know their rights.
39 |
40 | Developers that use the GNU GPL protect your rights with two steps:
41 | (1) assert copyright on the software, and (2) offer you this License
42 | giving you legal permission to copy, distribute and/or modify it.
43 |
44 | For the developers' and authors' protection, the GPL clearly explains
45 | that there is no warranty for this free software. For both users' and
46 | authors' sake, the GPL requires that modified versions be marked as
47 | changed, so that their problems will not be attributed erroneously to
48 | authors of previous versions.
49 |
50 | Some devices are designed to deny users access to install or run
51 | modified versions of the software inside them, although the manufacturer
52 | can do so. This is fundamentally incompatible with the aim of
53 | protecting users' freedom to change the software. The systematic
54 | pattern of such abuse occurs in the area of products for individuals to
55 | use, which is precisely where it is most unacceptable. Therefore, we
56 | have designed this version of the GPL to prohibit the practice for those
57 | products. If such problems arise substantially in other domains, we
58 | stand ready to extend this provision to those domains in future versions
59 | of the GPL, as needed to protect the freedom of users.
60 |
61 | Finally, every program is threatened constantly by software patents.
62 | States should not allow patents to restrict development and use of
63 | software on general-purpose computers, but in those that do, we wish to
64 | avoid the special danger that patents applied to a free program could
65 | make it effectively proprietary. To prevent this, the GPL assures that
66 | patents cannot be used to render the program non-free.
67 |
68 | The precise terms and conditions for copying, distribution and
69 | modification follow.
70 |
71 | TERMS AND CONDITIONS
72 |
73 | 0. Definitions.
74 |
75 | "This License" refers to version 3 of the GNU General Public License.
76 |
77 | "Copyright" also means copyright-like laws that apply to other kinds of
78 | works, such as semiconductor masks.
79 |
80 | "The Program" refers to any copyrightable work licensed under this
81 | License. Each licensee is addressed as "you". "Licensees" and
82 | "recipients" may be individuals or organizations.
83 |
84 | To "modify" a work means to copy from or adapt all or part of the work
85 | in a fashion requiring copyright permission, other than the making of an
86 | exact copy. The resulting work is called a "modified version" of the
87 | earlier work or a work "based on" the earlier work.
88 |
89 | A "covered work" means either the unmodified Program or a work based
90 | on the Program.
91 |
92 | To "propagate" a work means to do anything with it that, without
93 | permission, would make you directly or secondarily liable for
94 | infringement under applicable copyright law, except executing it on a
95 | computer or modifying a private copy. Propagation includes copying,
96 | distribution (with or without modification), making available to the
97 | public, and in some countries other activities as well.
98 |
99 | To "convey" a work means any kind of propagation that enables other
100 | parties to make or receive copies. Mere interaction with a user through
101 | a computer network, with no transfer of a copy, is not conveying.
102 |
103 | An interactive user interface displays "Appropriate Legal Notices"
104 | to the extent that it includes a convenient and prominently visible
105 | feature that (1) displays an appropriate copyright notice, and (2)
106 | tells the user that there is no warranty for the work (except to the
107 | extent that warranties are provided), that licensees may convey the
108 | work under this License, and how to view a copy of this License. If
109 | the interface presents a list of user commands or options, such as a
110 | menu, a prominent item in the list meets this criterion.
111 |
112 | 1. Source Code.
113 |
114 | The "source code" for a work means the preferred form of the work
115 | for making modifications to it. "Object code" means any non-source
116 | form of a work.
117 |
118 | A "Standard Interface" means an interface that either is an official
119 | standard defined by a recognized standards body, or, in the case of
120 | interfaces specified for a particular programming language, one that
121 | is widely used among developers working in that language.
122 |
123 | The "System Libraries" of an executable work include anything, other
124 | than the work as a whole, that (a) is included in the normal form of
125 | packaging a Major Component, but which is not part of that Major
126 | Component, and (b) serves only to enable use of the work with that
127 | Major Component, or to implement a Standard Interface for which an
128 | implementation is available to the public in source code form. A
129 | "Major Component", in this context, means a major essential component
130 | (kernel, window system, and so on) of the specific operating system
131 | (if any) on which the executable work runs, or a compiler used to
132 | produce the work, or an object code interpreter used to run it.
133 |
134 | The "Corresponding Source" for a work in object code form means all
135 | the source code needed to generate, install, and (for an executable
136 | work) run the object code and to modify the work, including scripts to
137 | control those activities. However, it does not include the work's
138 | System Libraries, or general-purpose tools or generally available free
139 | programs which are used unmodified in performing those activities but
140 | which are not part of the work. For example, Corresponding Source
141 | includes interface definition files associated with source files for
142 | the work, and the source code for shared libraries and dynamically
143 | linked subprograms that the work is specifically designed to require,
144 | such as by intimate data communication or control flow between those
145 | subprograms and other parts of the work.
146 |
147 | The Corresponding Source need not include anything that users
148 | can regenerate automatically from other parts of the Corresponding
149 | Source.
150 |
151 | The Corresponding Source for a work in source code form is that
152 | same work.
153 |
154 | 2. Basic Permissions.
155 |
156 | All rights granted under this License are granted for the term of
157 | copyright on the Program, and are irrevocable provided the stated
158 | conditions are met. This License explicitly affirms your unlimited
159 | permission to run the unmodified Program. The output from running a
160 | covered work is covered by this License only if the output, given its
161 | content, constitutes a covered work. This License acknowledges your
162 | rights of fair use or other equivalent, as provided by copyright law.
163 |
164 | You may make, run and propagate covered works that you do not
165 | convey, without conditions so long as your license otherwise remains
166 | in force. You may convey covered works to others for the sole purpose
167 | of having them make modifications exclusively for you, or provide you
168 | with facilities for running those works, provided that you comply with
169 | the terms of this License in conveying all material for which you do
170 | not control copyright. Those thus making or running the covered works
171 | for you must do so exclusively on your behalf, under your direction
172 | and control, on terms that prohibit them from making any copies of
173 | your copyrighted material outside their relationship with you.
174 |
175 | Conveying under any other circumstances is permitted solely under
176 | the conditions stated below. Sublicensing is not allowed; section 10
177 | makes it unnecessary.
178 |
179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180 |
181 | No covered work shall be deemed part of an effective technological
182 | measure under any applicable law fulfilling obligations under article
183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 | similar laws prohibiting or restricting circumvention of such
185 | measures.
186 |
187 | When you convey a covered work, you waive any legal power to forbid
188 | circumvention of technological measures to the extent such circumvention
189 | is effected by exercising rights under this License with respect to
190 | the covered work, and you disclaim any intention to limit operation or
191 | modification of the work as a means of enforcing, against the work's
192 | users, your or third parties' legal rights to forbid circumvention of
193 | technological measures.
194 |
195 | 4. Conveying Verbatim Copies.
196 |
197 | You may convey verbatim copies of the Program's source code as you
198 | receive it, in any medium, provided that you conspicuously and
199 | appropriately publish on each copy an appropriate copyright notice;
200 | keep intact all notices stating that this License and any
201 | non-permissive terms added in accord with section 7 apply to the code;
202 | keep intact all notices of the absence of any warranty; and give all
203 | recipients a copy of this License along with the Program.
204 |
205 | You may charge any price or no price for each copy that you convey,
206 | and you may offer support or warranty protection for a fee.
207 |
208 | 5. Conveying Modified Source Versions.
209 |
210 | You may convey a work based on the Program, or the modifications to
211 | produce it from the Program, in the form of source code under the
212 | terms of section 4, provided that you also meet all of these conditions:
213 |
214 | a) The work must carry prominent notices stating that you modified
215 | it, and giving a relevant date.
216 |
217 | b) The work must carry prominent notices stating that it is
218 | released under this License and any conditions added under section
219 | 7. This requirement modifies the requirement in section 4 to
220 | "keep intact all notices".
221 |
222 | c) You must license the entire work, as a whole, under this
223 | License to anyone who comes into possession of a copy. This
224 | License will therefore apply, along with any applicable section 7
225 | additional terms, to the whole of the work, and all its parts,
226 | regardless of how they are packaged. This License gives no
227 | permission to license the work in any other way, but it does not
228 | invalidate such permission if you have separately received it.
229 |
230 | d) If the work has interactive user interfaces, each must display
231 | Appropriate Legal Notices; however, if the Program has interactive
232 | interfaces that do not display Appropriate Legal Notices, your
233 | work need not make them do so.
234 |
235 | A compilation of a covered work with other separate and independent
236 | works, which are not by their nature extensions of the covered work,
237 | and which are not combined with it such as to form a larger program,
238 | in or on a volume of a storage or distribution medium, is called an
239 | "aggregate" if the compilation and its resulting copyright are not
240 | used to limit the access or legal rights of the compilation's users
241 | beyond what the individual works permit. Inclusion of a covered work
242 | in an aggregate does not cause this License to apply to the other
243 | parts of the aggregate.
244 |
245 | 6. Conveying Non-Source Forms.
246 |
247 | You may convey a covered work in object code form under the terms
248 | of sections 4 and 5, provided that you also convey the
249 | machine-readable Corresponding Source under the terms of this License,
250 | in one of these ways:
251 |
252 | a) Convey the object code in, or embodied in, a physical product
253 | (including a physical distribution medium), accompanied by the
254 | Corresponding Source fixed on a durable physical medium
255 | customarily used for software interchange.
256 |
257 | b) Convey the object code in, or embodied in, a physical product
258 | (including a physical distribution medium), accompanied by a
259 | written offer, valid for at least three years and valid for as
260 | long as you offer spare parts or customer support for that product
261 | model, to give anyone who possesses the object code either (1) a
262 | copy of the Corresponding Source for all the software in the
263 | product that is covered by this License, on a durable physical
264 | medium customarily used for software interchange, for a price no
265 | more than your reasonable cost of physically performing this
266 | conveying of source, or (2) access to copy the
267 | Corresponding Source from a network server at no charge.
268 |
269 | c) Convey individual copies of the object code with a copy of the
270 | written offer to provide the Corresponding Source. This
271 | alternative is allowed only occasionally and noncommercially, and
272 | only if you received the object code with such an offer, in accord
273 | with subsection 6b.
274 |
275 | d) Convey the object code by offering access from a designated
276 | place (gratis or for a charge), and offer equivalent access to the
277 | Corresponding Source in the same way through the same place at no
278 | further charge. You need not require recipients to copy the
279 | Corresponding Source along with the object code. If the place to
280 | copy the object code is a network server, the Corresponding Source
281 | may be on a different server (operated by you or a third party)
282 | that supports equivalent copying facilities, provided you maintain
283 | clear directions next to the object code saying where to find the
284 | Corresponding Source. Regardless of what server hosts the
285 | Corresponding Source, you remain obligated to ensure that it is
286 | available for as long as needed to satisfy these requirements.
287 |
288 | e) Convey the object code using peer-to-peer transmission, provided
289 | you inform other peers where the object code and Corresponding
290 | Source of the work are being offered to the general public at no
291 | charge under subsection 6d.
292 |
293 | A separable portion of the object code, whose source code is excluded
294 | from the Corresponding Source as a System Library, need not be
295 | included in conveying the object code work.
296 |
297 | A "User Product" is either (1) a "consumer product", which means any
298 | tangible personal property which is normally used for personal, family,
299 | or household purposes, or (2) anything designed or sold for incorporation
300 | into a dwelling. In determining whether a product is a consumer product,
301 | doubtful cases shall be resolved in favor of coverage. For a particular
302 | product received by a particular user, "normally used" refers to a
303 | typical or common use of that class of product, regardless of the status
304 | of the particular user or of the way in which the particular user
305 | actually uses, or expects or is expected to use, the product. A product
306 | is a consumer product regardless of whether the product has substantial
307 | commercial, industrial or non-consumer uses, unless such uses represent
308 | the only significant mode of use of the product.
309 |
310 | "Installation Information" for a User Product means any methods,
311 | procedures, authorization keys, or other information required to install
312 | and execute modified versions of a covered work in that User Product from
313 | a modified version of its Corresponding Source. The information must
314 | suffice to ensure that the continued functioning of the modified object
315 | code is in no case prevented or interfered with solely because
316 | modification has been made.
317 |
318 | If you convey an object code work under this section in, or with, or
319 | specifically for use in, a User Product, and the conveying occurs as
320 | part of a transaction in which the right of possession and use of the
321 | User Product is transferred to the recipient in perpetuity or for a
322 | fixed term (regardless of how the transaction is characterized), the
323 | Corresponding Source conveyed under this section must be accompanied
324 | by the Installation Information. But this requirement does not apply
325 | if neither you nor any third party retains the ability to install
326 | modified object code on the User Product (for example, the work has
327 | been installed in ROM).
328 |
329 | The requirement to provide Installation Information does not include a
330 | requirement to continue to provide support service, warranty, or updates
331 | for a work that has been modified or installed by the recipient, or for
332 | the User Product in which it has been modified or installed. Access to a
333 | network may be denied when the modification itself materially and
334 | adversely affects the operation of the network or violates the rules and
335 | protocols for communication across the network.
336 |
337 | Corresponding Source conveyed, and Installation Information provided,
338 | in accord with this section must be in a format that is publicly
339 | documented (and with an implementation available to the public in
340 | source code form), and must require no special password or key for
341 | unpacking, reading or copying.
342 |
343 | 7. Additional Terms.
344 |
345 | "Additional permissions" are terms that supplement the terms of this
346 | License by making exceptions from one or more of its conditions.
347 | Additional permissions that are applicable to the entire Program shall
348 | be treated as though they were included in this License, to the extent
349 | that they are valid under applicable law. If additional permissions
350 | apply only to part of the Program, that part may be used separately
351 | under those permissions, but the entire Program remains governed by
352 | this License without regard to the additional permissions.
353 |
354 | When you convey a copy of a covered work, you may at your option
355 | remove any additional permissions from that copy, or from any part of
356 | it. (Additional permissions may be written to require their own
357 | removal in certain cases when you modify the work.) You may place
358 | additional permissions on material, added by you to a covered work,
359 | for which you have or can give appropriate copyright permission.
360 |
361 | Notwithstanding any other provision of this License, for material you
362 | add to a covered work, you may (if authorized by the copyright holders of
363 | that material) supplement the terms of this License with terms:
364 |
365 | a) Disclaiming warranty or limiting liability differently from the
366 | terms of sections 15 and 16 of this License; or
367 |
368 | b) Requiring preservation of specified reasonable legal notices or
369 | author attributions in that material or in the Appropriate Legal
370 | Notices displayed by works containing it; or
371 |
372 | c) Prohibiting misrepresentation of the origin of that material, or
373 | requiring that modified versions of such material be marked in
374 | reasonable ways as different from the original version; or
375 |
376 | d) Limiting the use for publicity purposes of names of licensors or
377 | authors of the material; or
378 |
379 | e) Declining to grant rights under trademark law for use of some
380 | trade names, trademarks, or service marks; or
381 |
382 | f) Requiring indemnification of licensors and authors of that
383 | material by anyone who conveys the material (or modified versions of
384 | it) with contractual assumptions of liability to the recipient, for
385 | any liability that these contractual assumptions directly impose on
386 | those licensors and authors.
387 |
388 | All other non-permissive additional terms are considered "further
389 | restrictions" within the meaning of section 10. If the Program as you
390 | received it, or any part of it, contains a notice stating that it is
391 | governed by this License along with a term that is a further
392 | restriction, you may remove that term. If a license document contains
393 | a further restriction but permits relicensing or conveying under this
394 | License, you may add to a covered work material governed by the terms
395 | of that license document, provided that the further restriction does
396 | not survive such relicensing or conveying.
397 |
398 | If you add terms to a covered work in accord with this section, you
399 | must place, in the relevant source files, a statement of the
400 | additional terms that apply to those files, or a notice indicating
401 | where to find the applicable terms.
402 |
403 | Additional terms, permissive or non-permissive, may be stated in the
404 | form of a separately written license, or stated as exceptions;
405 | the above requirements apply either way.
406 |
407 | 8. Termination.
408 |
409 | You may not propagate or modify a covered work except as expressly
410 | provided under this License. Any attempt otherwise to propagate or
411 | modify it is void, and will automatically terminate your rights under
412 | this License (including any patent licenses granted under the third
413 | paragraph of section 11).
414 |
415 | However, if you cease all violation of this License, then your
416 | license from a particular copyright holder is reinstated (a)
417 | provisionally, unless and until the copyright holder explicitly and
418 | finally terminates your license, and (b) permanently, if the copyright
419 | holder fails to notify you of the violation by some reasonable means
420 | prior to 60 days after the cessation.
421 |
422 | Moreover, your license from a particular copyright holder is
423 | reinstated permanently if the copyright holder notifies you of the
424 | violation by some reasonable means, this is the first time you have
425 | received notice of violation of this License (for any work) from that
426 | copyright holder, and you cure the violation prior to 30 days after
427 | your receipt of the notice.
428 |
429 | Termination of your rights under this section does not terminate the
430 | licenses of parties who have received copies or rights from you under
431 | this License. If your rights have been terminated and not permanently
432 | reinstated, you do not qualify to receive new licenses for the same
433 | material under section 10.
434 |
435 | 9. Acceptance Not Required for Having Copies.
436 |
437 | You are not required to accept this License in order to receive or
438 | run a copy of the Program. Ancillary propagation of a covered work
439 | occurring solely as a consequence of using peer-to-peer transmission
440 | to receive a copy likewise does not require acceptance. However,
441 | nothing other than this License grants you permission to propagate or
442 | modify any covered work. These actions infringe copyright if you do
443 | not accept this License. Therefore, by modifying or propagating a
444 | covered work, you indicate your acceptance of this License to do so.
445 |
446 | 10. Automatic Licensing of Downstream Recipients.
447 |
448 | Each time you convey a covered work, the recipient automatically
449 | receives a license from the original licensors, to run, modify and
450 | propagate that work, subject to this License. You are not responsible
451 | for enforcing compliance by third parties with this License.
452 |
453 | An "entity transaction" is a transaction transferring control of an
454 | organization, or substantially all assets of one, or subdividing an
455 | organization, or merging organizations. If propagation of a covered
456 | work results from an entity transaction, each party to that
457 | transaction who receives a copy of the work also receives whatever
458 | licenses to the work the party's predecessor in interest had or could
459 | give under the previous paragraph, plus a right to possession of the
460 | Corresponding Source of the work from the predecessor in interest, if
461 | the predecessor has it or can get it with reasonable efforts.
462 |
463 | You may not impose any further restrictions on the exercise of the
464 | rights granted or affirmed under this License. For example, you may
465 | not impose a license fee, royalty, or other charge for exercise of
466 | rights granted under this License, and you may not initiate litigation
467 | (including a cross-claim or counterclaim in a lawsuit) alleging that
468 | any patent claim is infringed by making, using, selling, offering for
469 | sale, or importing the Program or any portion of it.
470 |
471 | 11. Patents.
472 |
473 | A "contributor" is a copyright holder who authorizes use under this
474 | License of the Program or a work on which the Program is based. The
475 | work thus licensed is called the contributor's "contributor version".
476 |
477 | A contributor's "essential patent claims" are all patent claims
478 | owned or controlled by the contributor, whether already acquired or
479 | hereafter acquired, that would be infringed by some manner, permitted
480 | by this License, of making, using, or selling its contributor version,
481 | but do not include claims that would be infringed only as a
482 | consequence of further modification of the contributor version. For
483 | purposes of this definition, "control" includes the right to grant
484 | patent sublicenses in a manner consistent with the requirements of
485 | this License.
486 |
487 | Each contributor grants you a non-exclusive, worldwide, royalty-free
488 | patent license under the contributor's essential patent claims, to
489 | make, use, sell, offer for sale, import and otherwise run, modify and
490 | propagate the contents of its contributor version.
491 |
492 | In the following three paragraphs, a "patent license" is any express
493 | agreement or commitment, however denominated, not to enforce a patent
494 | (such as an express permission to practice a patent or covenant not to
495 | sue for patent infringement). To "grant" such a patent license to a
496 | party means to make such an agreement or commitment not to enforce a
497 | patent against the party.
498 |
499 | If you convey a covered work, knowingly relying on a patent license,
500 | and the Corresponding Source of the work is not available for anyone
501 | to copy, free of charge and under the terms of this License, through a
502 | publicly available network server or other readily accessible means,
503 | then you must either (1) cause the Corresponding Source to be so
504 | available, or (2) arrange to deprive yourself of the benefit of the
505 | patent license for this particular work, or (3) arrange, in a manner
506 | consistent with the requirements of this License, to extend the patent
507 | license to downstream recipients. "Knowingly relying" means you have
508 | actual knowledge that, but for the patent license, your conveying the
509 | covered work in a country, or your recipient's use of the covered work
510 | in a country, would infringe one or more identifiable patents in that
511 | country that you have reason to believe are valid.
512 |
513 | If, pursuant to or in connection with a single transaction or
514 | arrangement, you convey, or propagate by procuring conveyance of, a
515 | covered work, and grant a patent license to some of the parties
516 | receiving the covered work authorizing them to use, propagate, modify
517 | or convey a specific copy of the covered work, then the patent license
518 | you grant is automatically extended to all recipients of the covered
519 | work and works based on it.
520 |
521 | A patent license is "discriminatory" if it does not include within
522 | the scope of its coverage, prohibits the exercise of, or is
523 | conditioned on the non-exercise of one or more of the rights that are
524 | specifically granted under this License. You may not convey a covered
525 | work if you are a party to an arrangement with a third party that is
526 | in the business of distributing software, under which you make payment
527 | to the third party based on the extent of your activity of conveying
528 | the work, and under which the third party grants, to any of the
529 | parties who would receive the covered work from you, a discriminatory
530 | patent license (a) in connection with copies of the covered work
531 | conveyed by you (or copies made from those copies), or (b) primarily
532 | for and in connection with specific products or compilations that
533 | contain the covered work, unless you entered into that arrangement,
534 | or that patent license was granted, prior to 28 March 2007.
535 |
536 | Nothing in this License shall be construed as excluding or limiting
537 | any implied license or other defenses to infringement that may
538 | otherwise be available to you under applicable patent law.
539 |
540 | 12. No Surrender of Others' Freedom.
541 |
542 | If conditions are imposed on you (whether by court order, agreement or
543 | otherwise) that contradict the conditions of this License, they do not
544 | excuse you from the conditions of this License. If you cannot convey a
545 | covered work so as to satisfy simultaneously your obligations under this
546 | License and any other pertinent obligations, then as a consequence you may
547 | not convey it at all. For example, if you agree to terms that obligate you
548 | to collect a royalty for further conveying from those to whom you convey
549 | the Program, the only way you could satisfy both those terms and this
550 | License would be to refrain entirely from conveying the Program.
551 |
552 | 13. Use with the GNU Affero General Public License.
553 |
554 | Notwithstanding any other provision of this License, you have
555 | permission to link or combine any covered work with a work licensed
556 | under version 3 of the GNU Affero General Public License into a single
557 | combined work, and to convey the resulting work. The terms of this
558 | License will continue to apply to the part which is the covered work,
559 | but the special requirements of the GNU Affero General Public License,
560 | section 13, concerning interaction through a network will apply to the
561 | combination as such.
562 |
563 | 14. Revised Versions of this License.
564 |
565 | The Free Software Foundation may publish revised and/or new versions of
566 | the GNU General Public License from time to time. Such new versions will
567 | be similar in spirit to the present version, but may differ in detail to
568 | address new problems or concerns.
569 |
570 | Each version is given a distinguishing version number. If the
571 | Program specifies that a certain numbered version of the GNU General
572 | Public License "or any later version" applies to it, you have the
573 | option of following the terms and conditions either of that numbered
574 | version or of any later version published by the Free Software
575 | Foundation. If the Program does not specify a version number of the
576 | GNU General Public License, you may choose any version ever published
577 | by the Free Software Foundation.
578 |
579 | If the Program specifies that a proxy can decide which future
580 | versions of the GNU General Public License can be used, that proxy's
581 | public statement of acceptance of a version permanently authorizes you
582 | to choose that version for the Program.
583 |
584 | Later license versions may give you additional or different
585 | permissions. However, no additional obligations are imposed on any
586 | author or copyright holder as a result of your choosing to follow a
587 | later version.
588 |
589 | 15. Disclaimer of Warranty.
590 |
591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599 |
600 | 16. Limitation of Liability.
601 |
602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 | SUCH DAMAGES.
611 |
612 | 17. Interpretation of Sections 15 and 16.
613 |
614 | If the disclaimer of warranty and limitation of liability provided
615 | above cannot be given local legal effect according to their terms,
616 | reviewing courts shall apply local law that most closely approximates
617 | an absolute waiver of all civil liability in connection with the
618 | Program, unless a warranty or assumption of liability accompanies a
619 | copy of the Program in return for a fee.
620 |
621 | END OF TERMS AND CONDITIONS
622 |
623 | How to Apply These Terms to Your New Programs
624 |
625 | If you develop a new program, and you want it to be of the greatest
626 | possible use to the public, the best way to achieve this is to make it
627 | free software which everyone can redistribute and change under these terms.
628 |
629 | To do so, attach the following notices to the program. It is safest
630 | to attach them to the start of each source file to most effectively
631 | state the exclusion of warranty; and each file should have at least
632 | the "copyright" line and a pointer to where the full notice is found.
633 |
634 |
635 | Copyright (C)
636 |
637 | This program is free software: you can redistribute it and/or modify
638 | it under the terms of the GNU General Public License as published by
639 | the Free Software Foundation, either version 3 of the License, or
640 | (at your option) any later version.
641 |
642 | This program is distributed in the hope that it will be useful,
643 | but WITHOUT ANY WARRANTY; without even the implied warranty of
644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 | GNU General Public License for more details.
646 |
647 | You should have received a copy of the GNU General Public License
648 | along with this program. If not, see .
649 |
650 | Also add information on how to contact you by electronic and paper mail.
651 |
652 | If the program does terminal interaction, make it output a short
653 | notice like this when it starts in an interactive mode:
654 |
655 | Copyright (C)
656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands `show w' and `show c' should show the appropriate
661 | parts of the General Public License. Of course, your program's commands
662 | might be different; for a GUI interface, you would use an "about box".
663 |
664 | You should also get your employer (if you work as a programmer) or school,
665 | if any, to sign a "copyright disclaimer" for the program, if necessary.
666 | For more information on this, and how to apply and follow the GNU GPL, see
667 | .
668 |
669 | The GNU General Public License does not permit incorporating your program
670 | into proprietary programs. If your program is a subroutine library, you
671 | may consider it more useful to permit linking proprietary applications with
672 | the library. If this is what you want to do, use the GNU Lesser General
673 | Public License instead of this License. But first, please read
674 | .
675 |
--------------------------------------------------------------------------------