├── README.md ├── LICENSE └── nft.py /README.md: -------------------------------------------------------------------------------- 1 | # custom_bilibili_nft 头图寄了已经改过的人千万别动了,动了就被还原了 头像还可以改 2 | 3 | 自定义 B 站 NFT 空间背景和头像 4 | 5 | ## 运行环境 6 | 7 | python3.7+ 8 | 9 | ## 安装依赖库 10 | 11 | ```bash 12 | pip install requests requests_toolbelt 13 | ``` 14 | 15 | ## 获取 ACCESS_KEY 16 | 17 | https://github.com/XiaoMiku01/fansMedalHelper/releases/tag/logintool 18 | 19 | ## 填写配置后运行 20 | 21 | ## 注意 22 | 23 | - 必须有个胶囊计划 R 级别的卡 (闲鱼 2-3 块钱一个,关键词:胶囊计划) ,其他集合自行更换 第 81 行的 `act_id` 参数 24 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /nft.py: -------------------------------------------------------------------------------- 1 | import time 2 | from typing import Union 3 | import requests 4 | from hashlib import md5 5 | from typing import Union 6 | from urllib.parse import urlencode 7 | from requests_toolbelt.multipart.encoder import MultipartEncoder 8 | import imghdr 9 | 10 | UID = 0 # 你的UID 11 | ACCESS_KEY = "" # 你的ACCESS_KEY (TV端,非TV端自行更换下面的APPKEY,APPSECRET,TV端access_key获取工具 https://github.com/XiaoMiku01/fansMedalHelper/releases/tag/logintool) 12 | FACE_PATH = r"face.jpg" # 头像路径 推荐正方形 13 | BG_PATH = r"background.jpg" # 背景图路径 推荐 9:16 竖版原图 效果非常好 14 | 15 | 16 | class Crypto: 17 | 18 | APPKEY = '4409e2ce8ffd12b8' 19 | APPSECRET = '59b43e04ad6965f34319062b478f83dd' 20 | 21 | @staticmethod 22 | def md5(data: Union[str, bytes]) -> str: 23 | '''generates md5 hex dump of `str` or `bytes`''' 24 | if type(data) == str: 25 | return md5(data.encode()).hexdigest() 26 | return md5(data).hexdigest() 27 | 28 | @staticmethod 29 | def sign(data: Union[str, dict]) -> str: 30 | '''salted sign funtion for `dict`(converts to qs then parse) & `str`''' 31 | if isinstance(data, dict): 32 | _str = urlencode(data) 33 | elif type(data) != str: 34 | raise TypeError 35 | return Crypto.md5(_str + Crypto.APPSECRET) 36 | 37 | 38 | class SingableDict(dict): 39 | @property 40 | def sorted(self): 41 | '''returns a alphabetically sorted version of `self`''' 42 | return dict(sorted(self.items())) 43 | 44 | @property 45 | def signed(self): 46 | '''returns our sorted self with calculated `sign` as a new key-value pair at the end''' 47 | _sorted = self.sorted 48 | return {**_sorted, 'sign': Crypto.sign(_sorted)} 49 | 50 | 51 | def get_image_type(file_path): 52 | with open(file_path, 'rb') as f: 53 | data = f.read() 54 | return imghdr.what(None, data) 55 | 56 | 57 | def upload_image(file_path): 58 | url = "https://api.bilibili.com/x/upload/app/image?access_key=" + ACCESS_KEY 59 | 60 | payload = {'bucket': 'medialist', 'dir': 'nft'} 61 | 62 | with open(file_path, 'rb') as f: 63 | type = f'image/{imghdr.what(f)}' 64 | print(type) 65 | files = [ 66 | ( 67 | 'file', 68 | (file_path, f, type), 69 | ) 70 | ] 71 | response = requests.request("POST", url, data=payload, files=files) 72 | print(response.text) 73 | return response.json()['data']['location'] 74 | 75 | 76 | def get_one_card_id(): 77 | url = "https://api.bilibili.com/x/vas/nftcard/cardlist" 78 | params = SingableDict( 79 | { 80 | "access_key": ACCESS_KEY, 81 | "act_id": "4", 82 | "appkey": "4409e2ce8ffd12b8", 83 | "disable_rcmd": "0", 84 | "ruid": UID, 85 | "statistics": "{\"appId\":1,\"platform\":3,\"version\":\"7.9.0\",\"abtest\":\"\"}", 86 | "ts": int(time.time()), 87 | } 88 | ).signed 89 | response = requests.request("GET", url, params=params) 90 | data = response.json() 91 | if data['code'] != 0: 92 | print(data) 93 | return 94 | for round in data['data']['round_list']: 95 | for card in round['card_list']: 96 | if card['card_type'] == 1 and card['card_id_list']: 97 | print(card['card_id_list'][0]['card_id']) 98 | return card['card_id_list'][0]['card_id'] 99 | print('没有 R 级别胶囊计划的卡片') 100 | return None 101 | 102 | 103 | def set_face(card_id): 104 | api = "https://api.bilibili.com/x/member/app/face/digitalKit/update" 105 | params = SingableDict( 106 | { 107 | "access_key": ACCESS_KEY, 108 | "appkey": "4409e2ce8ffd12b8", 109 | "build": "7090300", 110 | "c_locale": "zh_CN", 111 | "channel": "xiaomi", 112 | "disable_rcmd": "0", 113 | "mobi_app": "android", 114 | "platform": "android", 115 | "s_locale": "zh_CN", 116 | "statistics": "{\"appId\":1,\"platform\":3,\"version\":\"7.9.0\",\"abtest\":\"\"}", 117 | "ts": int(time.time()), 118 | } 119 | ).signed 120 | m = MultipartEncoder( 121 | fields={ 122 | 'digital_kit_id': str(card_id), 123 | 'face': ('face', open(FACE_PATH, 'rb'), 'application/octet-stream'), 124 | } 125 | ) 126 | headers = { 127 | "Content-Type": m.content_type, 128 | } 129 | response = requests.request("POST", api, data=m, headers=headers, params=params) 130 | if response.json()['code'] != 0: 131 | print(response.json()) 132 | return 133 | print('设置头像成功, 请等待审核') 134 | 135 | 136 | def set_bg_img(img_url, card_id): 137 | api = "https://app.bilibili.com//x/v2/space/digital/bind" 138 | data = SingableDict( 139 | { 140 | "access_key": ACCESS_KEY, 141 | "appkey": "4409e2ce8ffd12b8", 142 | "build": "7090300", 143 | "c_locale": "zh_CN", 144 | "card_id": card_id, 145 | "channel": "xiaomi", 146 | "disable_rcmd": "0", 147 | "img_url": img_url, 148 | "mobi_app": "android", 149 | "platform": "android", 150 | "s_locale": "zh_CN", 151 | "space_bg_type": "1", 152 | "statistics": "{\"appId\":1,\"platform\":3,\"version\":\"7.9.0\",\"abtest\":\"\"}", 153 | "ts": int(time.time()), 154 | } 155 | ).signed 156 | headers = { 157 | "Content-Type": "application/x-www-form-urlencoded; charset=utf-8", 158 | } 159 | response = requests.request("POST", api, data=data, headers=headers) 160 | if response.json()['code'] != 0: 161 | print(response.json()) 162 | return 163 | print('设置背景成功') 164 | 165 | 166 | def main(): 167 | card_id = get_one_card_id() 168 | if not card_id: 169 | return 170 | # img_url = upload_image(BG_PATH) 171 | # set_bg_img(img_url, card_id) 172 | set_face(card_id) 173 | 174 | 175 | if __name__ == '__main__': 176 | main() 177 | --------------------------------------------------------------------------------