├── .gitignore ├── LICENSE ├── README.md ├── face.jpg └── nft.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 一心向晚 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # custom_bilibili_nft 2 | 3 | 自定义 B 站 NFT 空间背景和头像全自动版本 4 | 5 | 原项目:[XiaoMiku01/custom_bilibili_nft](https://github.com/XiaoMiku01/custom_bilibili_nft) 6 | 7 | 该项目在原有基础上增加扫码登陆,无需手动输入 uid 跟 access_key 8 | 9 | ## 运行环境 10 | 11 | python3.8+ 12 | 13 | ## 安装依赖库 14 | 15 | ```bash 16 | pip install requests requests_toolbelt qrcode 17 | ``` 18 | ## 注意 19 | 20 | - 必须有个 R 级别的卡 (可通过在手机端 B 站搜索三体在三体页面获取) ,其他集合自行更换 第 136 行的 `act_id` 参数 21 | -------------------------------------------------------------------------------- /face.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purofle/custom_bilibili_nft/f9bc0ba99e7fec661a962a3b0792c771418a5d29/face.jpg -------------------------------------------------------------------------------- /nft.py: -------------------------------------------------------------------------------- 1 | import time 2 | from typing import Union 3 | import requests 4 | from hashlib import md5 5 | from urllib.parse import urlencode 6 | from requests_toolbelt.multipart.encoder import MultipartEncoder 7 | import imghdr 8 | import json 9 | import qrcode 10 | import io 11 | 12 | UID = 0 # 你的UID 13 | # TV端access_key获取工具 https://github.com/XiaoMiku01/fansMedalHelper/releases/tag/logintool) 14 | ACCESS_KEY = "" # 你的ACCESS_KEY (TV端,非TV端自行更换下面的APPKEY,APPSECRET 15 | FACE_PATH = r"face.jpg" # 头像路径 推荐正方形 16 | BG_PATH = r"background.jpg" # 背景图路径 推荐 9:16 竖版原图 效果非常好 17 | 18 | 19 | class Crypto: 20 | 21 | APPKEY = '4409e2ce8ffd12b8' 22 | APPSECRET = '59b43e04ad6965f34319062b478f83dd' 23 | 24 | @staticmethod 25 | def md5(data: Union[str, bytes]) -> str: 26 | '''generates md5 hex dump of `str` or `bytes`''' 27 | if type(data) == str: 28 | return md5(data.encode()).hexdigest() 29 | return md5(data).hexdigest() 30 | 31 | @staticmethod 32 | def sign(data: Union[str, dict]) -> str: 33 | '''salted sign funtion for `dict`(converts to qs then parse) & `str`''' 34 | if isinstance(data, dict): 35 | _str = urlencode(data) 36 | elif type(data) != str: 37 | raise TypeError 38 | return Crypto.md5(_str + Crypto.APPSECRET) 39 | 40 | class SingableDict(dict): 41 | @property 42 | def sorted(self): 43 | '''returns a alphabetically sorted version of `self`''' 44 | return dict(sorted(self.items())) 45 | 46 | @property 47 | def signed(self): 48 | '''returns our sorted self with calculated `sign` as a new key-value pair at the end''' 49 | _sorted = self.sorted 50 | return {**_sorted, 'sign': Crypto.sign(_sorted)} 51 | 52 | def get_auth_code(): 53 | url = "https://passport.snm0516.aisee.tv/x/passport-tv-login/qrcode/auth_code" 54 | params = SingableDict({ 55 | "appkey": Crypto.APPKEY, 56 | "local_id": 0, 57 | "ts": int(time.time()) 58 | }).signed 59 | return requests.post(url, params=params).json()["data"] 60 | 61 | def tv_qr_login(auth_code: str): 62 | url = "https://passport.snm0516.aisee.tv/x/passport-tv-login/qrcode/poll" 63 | params = SingableDict({ 64 | "appkey": Crypto.APPKEY, 65 | "auth_code": auth_code, 66 | "local_id": 0, 67 | "ts": int(time.time()) 68 | }).signed 69 | return requests.post(url, params=params) 70 | 71 | def tv_login(): 72 | data = get_auth_code() 73 | qrcode_url = data["url"] 74 | auth_code = data["auth_code"] 75 | 76 | print("请扫描二维码或将下面的链接复制到哔哩哔哩内打开") 77 | print(qrcode_url) 78 | qr = qrcode.QRCode() 79 | qr.add_data(qrcode_url) 80 | f = io.StringIO() 81 | qr.print_ascii(out=f) 82 | f.seek(0) 83 | print(f.read()) 84 | 85 | access_token = "" 86 | uid = 0 87 | 88 | while access_token == "": 89 | try: 90 | login_data = tv_qr_login(auth_code).json() 91 | except Exception as e: 92 | print(f"炸了 {e}") 93 | print(login_data) 94 | if not login_data["data"] is None: 95 | access_token = login_data["data"]["access_token"] 96 | uid = login_data["data"]["mid"] 97 | break 98 | time.sleep(1.5) 99 | 100 | print(access_token) 101 | print(uid) 102 | 103 | global UID, ACCESS_KEY 104 | UID = uid 105 | ACCESS_KEY = access_token 106 | 107 | if UID == 0 and ACCESS_KEY == "": 108 | tv_login() 109 | 110 | 111 | def get_image_type(file_path): 112 | with open(file_path, 'rb') as f: 113 | data = f.read() 114 | return imghdr.what(None, data) 115 | 116 | 117 | def upload_image(file_path): 118 | url = "https://api.bilibili.com/x/upload/app/image?access_key=" + ACCESS_KEY 119 | 120 | payload = {'bucket': 'medialist', 'dir': 'nft'} 121 | 122 | with open(file_path, 'rb') as f: 123 | type = f'image/{imghdr.what(f)}' 124 | print(type) 125 | files = [ 126 | ( 127 | 'file', 128 | (file_path, f, type), 129 | ) 130 | ] 131 | response = requests.request("POST", url, data=payload, files=files) 132 | print(response.text) 133 | return response.json()['data']['location'] 134 | 135 | 136 | def get_one_card_id(): 137 | url = "https://api.bilibili.com/x/vas/nftcard/cardlist" 138 | params = SingableDict( 139 | { 140 | "access_key": ACCESS_KEY, 141 | "act_id": "14", 142 | "appkey": "4409e2ce8ffd12b8", 143 | "disable_rcmd": "0", 144 | "ruid": UID, 145 | "statistics": "{\"appId\":1,\"platform\":3,\"version\":\"7.9.0\",\"abtest\":\"\"}", 146 | "ts": int(time.time()), 147 | } 148 | ).signed 149 | response = requests.request("GET", url, params=params) 150 | data = response.json() 151 | if data['code'] != 0: 152 | print(data) 153 | return 154 | 155 | with open("data.json", "w") as f: 156 | f.write(json.dumps(data, indent=4, ensure_ascii=False)) 157 | 158 | # round_list 的 159 | for round in data['data']['round_list']: 160 | for card in round['card_list']: 161 | if card['card_type'] == 1 and card['card_id_list']: 162 | print(card['card_id_list'][0]['card_id']) 163 | return card['card_id_list'][0]['card_id'] 164 | 165 | # pre_list 的 166 | for card in data['data']['pre_list']: 167 | if card['card_type'] == 2 and card['card_id_list']: 168 | card_id = card['card_id_list'][0]['card_id'] 169 | print(card_id) 170 | return card_id 171 | print('没有 R 级别胶囊计划的卡片') 172 | return None 173 | 174 | 175 | def set_face(card_id): 176 | api = "https://api.bilibili.com/x/member/app/face/digitalKit/update" 177 | params = SingableDict( 178 | { 179 | "access_key": ACCESS_KEY, 180 | "appkey": "4409e2ce8ffd12b8", 181 | "build": "7090300", 182 | "c_locale": "zh_CN", 183 | "channel": "xiaomi", 184 | "disable_rcmd": "0", 185 | "mobi_app": "android", 186 | "platform": "android", 187 | "s_locale": "zh_CN", 188 | "statistics": "{\"appId\":1,\"platform\":3,\"version\":\"7.9.0\",\"abtest\":\"\"}", 189 | "ts": int(time.time()), 190 | } 191 | ).signed 192 | m = MultipartEncoder( 193 | fields={ 194 | 'digital_kit_id': str(card_id), 195 | 'face': ('face', open(FACE_PATH, 'rb'), 'application/octet-stream'), 196 | } 197 | ) 198 | headers = { 199 | "Content-Type": m.content_type, 200 | } 201 | response = requests.request("POST", api, data=m, headers=headers, params=params) 202 | if response.json()['code'] != 0: 203 | print(response.json()) 204 | return 205 | print('设置头像成功, 请等待审核') 206 | 207 | 208 | def set_bg_img(img_url, card_id): 209 | api = "https://app.bilibili.com//x/v2/space/digital/bind" 210 | data = SingableDict( 211 | { 212 | "access_key": ACCESS_KEY, 213 | "appkey": "4409e2ce8ffd12b8", 214 | "build": "7090300", 215 | "c_locale": "zh_CN", 216 | "card_id": card_id, 217 | "channel": "xiaomi", 218 | "disable_rcmd": "0", 219 | "img_url": img_url, 220 | "mobi_app": "android", 221 | "platform": "android", 222 | "s_locale": "zh_CN", 223 | "space_bg_type": "1", 224 | "statistics": "{\"appId\":1,\"platform\":3,\"version\":\"7.9.0\",\"abtest\":\"\"}", 225 | "ts": int(time.time()), 226 | } 227 | ).signed 228 | headers = { 229 | "Content-Type": "application/x-www-form-urlencoded; charset=utf-8", 230 | } 231 | response = requests.request("POST", api, data=data, headers=headers) 232 | if response.json()['code'] != 0: 233 | print(response.json()) 234 | return 235 | print('设置背景成功') 236 | 237 | 238 | def main(): 239 | card_id = get_one_card_id() 240 | if not card_id: 241 | return 242 | # img_url = upload_image(BG_PATH) 243 | # set_bg_img(img_url, card_id) 244 | set_face(card_id) 245 | 246 | 247 | if __name__ == '__main__': 248 | main() 249 | --------------------------------------------------------------------------------