From cb3f7ec9f171bfe3aecd4d4fd31869dcce756edb Mon Sep 17 00:00:00 2001 From: Hsury Date: Thu, 12 Dec 2019 14:30:00 +0800 Subject: [PATCH] =?UTF-8?q?=E9=80=82=E9=85=8D=E6=96=B0=E7=9A=84=E7=99=BB?= =?UTF-8?q?=E5=BD=95=E6=8E=A5=E5=8F=A3=E5=B9=B6=E4=BC=98=E5=8C=96=E6=8E=A7?= =?UTF-8?q?=E5=88=B6=E5=8F=B0=E7=9A=84=E8=BE=93=E5=87=BA=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .travis.yml | 2 +- README.md | 12 ++++----- bilibili.py | 7 +++-- drive.py | 74 +++++++++++++++++++++++++++-------------------------- 4 files changed, 50 insertions(+), 45 deletions(-) diff --git a/.travis.yml b/.travis.yml index fa12782..2bcf8be 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ if: 'tag IS blank' env: global: - - TRAVIS_TAG=v1.5 + - TRAVIS_TAG=v1.6 jobs: include: - diff --git a/README.md b/README.md index 44702fe..2176dd3 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@

☁️ 哔哩哔哩云,支持任意文件的全速上传与下载 ☁️

- +

@@ -20,7 +20,7 @@ - 轻量:无复杂依赖,资源占用少 - 自由:无文件格式与大小限制,无容量限制 -- 安全:上传的文件需要通过生成的META地址才能访问,他人无法随意查看 +- 安全:上传的文件需要通过生成的META URL才能访问,他人无法随意查看 - 稳定:带有分块校验与超时重试机制,在较差的网络环境中依然能确保文件的完整性 - 快速:支持多线程传输与断点续传,同时借助B站的CDN资源,能最大化地利用网络环境进行上传与下载 @@ -52,14 +52,14 @@ file: 待上传的文件路径 -t THREAD: 上传线程数, 默认值为4 ``` -上传完毕后,终端会打印一串META地址(通常以`bdrive://`开头)用于下载或分享,请妥善保管 +上传完毕后,终端会打印一串META URL(通常以`bdrive://`开头)用于下载或分享,请妥善保管 ### 下载 ``` drive.py download [-h] [-f] [-t THREAD] meta [file] -meta: META地址(通常以bdrive://开头) +meta: META URL(通常以bdrive://开头) file: 另存为新的文件名, 不指定则保存为上传时的文件名 -f: 覆盖已有文件 @@ -73,7 +73,7 @@ file: 另存为新的文件名, 不指定则保存为上传时的文件名 ``` drive.py info [-h] meta -meta: META地址(通常以bdrive://开头) +meta: META URL(通常以bdrive://开头) ``` ### 查看历史记录 @@ -106,7 +106,7 @@ drive.py history [-h] 分块:10 MB * 1492 -META地址:bdrive://d28784bff1086450a6c331fb322accccd382228e +META URL:bdrive://d28784bff1086450a6c331fb322accccd382228e ### 上传 diff --git a/bilibili.py b/bilibili.py index a1b06a6..dc52225 100644 --- a/bilibili.py +++ b/bilibili.py @@ -14,7 +14,7 @@ class Bilibili: def __init__(self): self._session = requests.Session() - self._session.headers.update({'User-Agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.75 Safari/537.36"}) + self._session.headers.update({'User-Agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.79 Safari/537.36"}) self.get_cookies = lambda: self._session.cookies.get_dict(domain=".bilibili.com") self.get_uid = lambda: self.get_cookies().get("DedeUserID", "") self.username = "" @@ -82,7 +82,7 @@ class Bilibili: while True: key = get_key() key_hash, pub_key = key['key_hash'], key['pub_key'] - url = f"https://passport.bilibili.com/api/v2/oauth2/login" + url = f"https://passport.bilibili.com/api/v3/oauth2/login" param = f"appkey={Bilibili.app_key}&password={parse.quote_plus(base64.b64encode(rsa.encrypt(f'{key_hash}{self.password}'.encode(), pub_key)))}&username={parse.quote_plus(self.username)}" payload = f"{param}&sign={self.calc_sign(param)}" headers = {'Content-type': "application/x-www-form-urlencoded"} @@ -107,6 +107,9 @@ class Bilibili: self._log(f"登录验证码识别服务暂时不可用, 10秒后重试") time.sleep(10) break + elif response['code'] == -449: + time.sleep(1) + response = self._requests("post", url, data=payload, headers=headers) elif response['code'] == 0 and response['data']['status'] == 0: for cookie in response['data']['cookie_info']['cookies']: self._session.cookies.set(cookie['name'], cookie['value'], domain=".bilibili.com") diff --git a/drive.py b/drive.py index db77707..76c1f58 100644 --- a/drive.py +++ b/drive.py @@ -22,6 +22,7 @@ bundle_dir = os.path.dirname(sys.executable) if getattr(sys, "frozen", False) el default_url = lambda sha1: f"http://i0.hdslb.com/bfs/album/{sha1}.x-ms-bmp" meta_string = lambda url: ("bdrive://" + re.findall(r"[a-fA-F0-9]{40}", url)[0]) if re.match(r"^http(s?)://i0.hdslb.com/bfs/album/[a-fA-F0-9]{40}.x-ms-bmp$", url) else url +size_string = lambda byte: f"{byte / 1024 / 1024 / 1024:.2f} GB" if byte > 1024 * 1024 * 1024 else f"{byte / 1024 / 1024:.2f} MB" if byte > 1024 * 1024 else f"{byte / 1024:.2f} KB" if byte > 1024 else f"{int(byte)} B" def bmp_header(data): return b"BM" \ @@ -69,7 +70,7 @@ def image_upload(data, cookies): headers = { 'Origin': "https://t.bilibili.com", 'Referer': "https://t.bilibili.com/", - 'User-Agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.70 Safari/537.36", + 'User-Agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.79 Safari/537.36", } files = { 'file_up': (f"{int(time.time() * 1000)}.bmp", data), @@ -77,7 +78,7 @@ def image_upload(data, cookies): 'category': "daily", } try: - response = requests.post(url, headers=headers, cookies=cookies, files=files).json() + response = requests.post(url, headers=headers, cookies=cookies, files=files, timeout=300).json() except: response = None return response @@ -85,12 +86,12 @@ def image_upload(data, cookies): def image_download(url): headers = { 'Referer': "http://t.bilibili.com/", - 'User-Agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.70 Safari/537.36", + 'User-Agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.79 Safari/537.36", } content = [] last_chunk_time = None try: - for chunk in requests.get(url, headers=headers, timeout=10, stream=True).iter_content(64 * 1024): + for chunk in requests.get(url, headers=headers, timeout=10, stream=True).iter_content(128 * 1024): if last_chunk_time is not None and time.time() - last_chunk_time > 5: return None content.append(chunk) @@ -125,9 +126,9 @@ def history_handle(args): history = read_history() if history: for index, meta_dict in enumerate(history.values()): - prefix = f"[{index}]" - print(f"{prefix} {meta_dict['filename']} ({meta_dict['size'] / 1024 / 1024:.2f} MB), 共有{len(meta_dict['block'])}个分块, 上传于{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(meta_dict['time']))}") - print(f"{' ' * len(prefix)} {meta_string(meta_dict['url'])}") + prefix = f"[{index + 1}]" + print(f"{prefix} {meta_dict['filename']} ({size_string(meta_dict['size'])}), 共有{len(meta_dict['block'])}个分块, 上传于{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(meta_dict['time']))}") + print(f"{' ' * len(prefix)} META URL -> {meta_string(meta_dict['url'])}") else: print(f"暂无历史记录") @@ -135,12 +136,12 @@ def info_handle(args): meta_dict = fetch_meta(args.meta) if meta_dict: print(f"文件名: {meta_dict['filename']}") - print(f"大小: {meta_dict['size'] / 1024 / 1024:.2f} MB") + print(f"大小: {size_string(meta_dict['size'])}") print(f"SHA-1: {meta_dict['sha1']}") print(f"上传时间: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(meta_dict['time']))}") print(f"分块数: {len(meta_dict['block'])}") for index, block_dict in enumerate(meta_dict['block']): - print(f"分块{index} ({block_dict['size'] / 1024 / 1024:.2f} MB) URL: {block_dict['url']}") + print(f"分块{index + 1} ({size_string(block_dict['size'])}) URL: {block_dict['url']}") else: print("元数据解析失败") @@ -159,14 +160,14 @@ def upload_handle(args): full_block_sha1 = calc_sha1(full_block, hexdigest=True) url = is_skippable(full_block_sha1) if url: - # log(f"分块{index} ({len(block) / 1024 / 1024:.2f} MB) 已存在于服务器") + log(f"分块{index + 1}/{block_num}上传完毕") block_dict[index] = { 'url': url, 'size': len(block), 'sha1': block_sha1, } else: - # log(f"分块{index} ({len(block) / 1024 / 1024:.2f} MB) 开始上传") + # log(f"分块{index + 1}/{block_num}开始上传") for _ in range(10): if terminate_flag.is_set(): return @@ -174,7 +175,7 @@ def upload_handle(args): if response: if response['code'] == 0: url = response['data']['image_url'] - log(f"分块{index} ({len(block) / 1024 / 1024:.2f} MB) 上传完毕") + log(f"分块{index + 1}/{block_num}上传完毕") block_dict[index] = { 'url': url, 'size': len(block), @@ -183,9 +184,9 @@ def upload_handle(args): return elif response['code'] == -4: terminate_flag.set() - log(f"分块{index} ({len(block) / 1024 / 1024:.2f} MB) 第{_ + 1}次上传失败, 请重新登录") + log(f"分块{index + 1}/{block_num}第{_ + 1}次上传失败, 请重新登录") return - log(f"分块{index} ({len(block) / 1024 / 1024:.2f} MB) 第{_ + 1}次上传失败") + log(f"分块{index + 1}/{block_num}第{_ + 1}次上传失败") else: terminate_flag.set() except: @@ -198,7 +199,7 @@ def upload_handle(args): url = default_url(sha1) headers = { 'Referer': "http://t.bilibili.com/", - 'User-Agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.70 Safari/537.36", + 'User-Agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.79 Safari/537.36", } for _ in range(5): try: @@ -218,18 +219,18 @@ def upload_handle(args): start_time = time.time() file_name = args.file if not os.path.exists(file_name): - log(f"{file_name}不存在") + log(f"文件{file_name}不存在") return None if os.path.isdir(file_name): - log("不支持上传文件夹") + log("暂不支持上传文件夹") return None - log(f"上传: {os.path.basename(file_name)} ({os.path.getsize(file_name) / 1024 / 1024:.2f} MB)") + log(f"上传: {os.path.basename(file_name)} ({size_string(os.path.getsize(file_name))})") first_4mb_sha1 = calc_sha1(read_in_chunk(file_name, chunk_size=4 * 1024 * 1024, chunk_number=1), hexdigest=True) history = read_history() if first_4mb_sha1 in history: url = history[first_4mb_sha1]['url'] - log(f"该文件已于{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(history[first_4mb_sha1]['time']))}上传, 共有{len(history[first_4mb_sha1]['block'])}个分块") - log(meta_string(url)) + log(f"文件已于{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(history[first_4mb_sha1]['time']))}上传, 共有{len(history[first_4mb_sha1]['block'])}个分块") + log(f"META URL -> {meta_string(url)}") return url try: with open(os.path.join(bundle_dir, "cookies.json"), "r", encoding="utf-8") as f: @@ -242,6 +243,7 @@ def upload_handle(args): terminate_flag = threading.Event() thread_pool = [] block_dict = {} + block_num = math.ceil(os.path.getsize(file_name) / (args.block_size * 1024 * 1024)) for index, block in enumerate(read_in_chunk(file_name, chunk_size=args.block_size * 1024 * 1024)): if len(thread_pool) >= args.thread: done_flag.acquire() @@ -270,8 +272,8 @@ def upload_handle(args): if response and response['code'] == 0: url = response['data']['image_url'] log("元数据上传完毕") - log(f"{os.path.basename(file_name)}上传完毕, 共有{len(meta_dict['block'])}个分块, 用时{int(time.time() - start_time)}秒, 平均速度{meta_dict['size'] / 1024 / 1024 / (time.time() - start_time):.2f} MB/s") - log(meta_string(url)) + log(f"{meta_dict['filename']} ({size_string(meta_dict['size'])}) 上传完毕, 用时{time.time() - start_time:.1f}秒, 平均速度{size_string(meta_dict['size'] / (time.time() - start_time))}/s") + log(f"META URL -> {meta_string(url)}") write_history(first_4mb_sha1, meta_dict, url) return url log(f"元数据第{_ + 1}次上传失败") @@ -281,7 +283,7 @@ def upload_handle(args): def download_handle(args): def core(index, block_dict): try: - # log(f"分块{index} ({block_dict['size'] / 1024 / 1024:.2f} MB) 开始下载") + # log(f"分块{index + 1}/{len(meta_dict['block'])}开始下载") for _ in range(10): if terminate_flag.is_set(): return @@ -293,12 +295,12 @@ def download_handle(args): f.seek(block_offset(index)) f.write(block) file_lock.release() - log(f"分块{index} ({block_dict['size'] / 1024 / 1024:.2f} MB) 下载完毕") + log(f"分块{index + 1}/{len(meta_dict['block'])}下载完毕") return else: - log(f"分块{index} ({block_dict['size'] / 1024 / 1024:.2f} MB) 校验未通过") + log(f"分块{index + 1}/{len(meta_dict['block'])}校验未通过") else: - log(f"分块{index} ({block_dict['size'] / 1024 / 1024:.2f} MB) 第{_ + 1}次下载失败") + log(f"分块{index + 1}/{len(meta_dict['block'])}第{_ + 1}次下载失败") else: terminate_flag.set() except: @@ -314,13 +316,13 @@ def download_handle(args): if args.force: return True else: - return (input(f"{os.path.basename(file_name)}已存在于本地, 是否覆盖? [y/N] ") in ["y", "Y"]) + return (input("文件已存在, 是否覆盖? [y/N] ") in ["y", "Y"]) start_time = time.time() meta_dict = fetch_meta(args.meta) if meta_dict: file_name = args.file if args.file else meta_dict['filename'] - log(f"下载: {os.path.basename(file_name)} ({meta_dict['size'] / 1024 / 1024:.2f} MB), 共有{len(meta_dict['block'])}个分块, 上传于{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(meta_dict['time']))}") + log(f"下载: {os.path.basename(file_name)} ({size_string(meta_dict['size'])}), 共有{len(meta_dict['block'])}个分块, 上传于{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(meta_dict['time']))}") else: log("元数据解析失败") return None @@ -328,19 +330,19 @@ def download_handle(args): download_block_list = [] if os.path.exists(file_name): if os.path.getsize(file_name) == meta_dict['size'] and calc_sha1(read_in_chunk(file_name), hexdigest=True) == meta_dict['sha1']: - log(f"{os.path.basename(file_name)}已存在于本地, 且与服务器端文件内容一致") + log("文件已存在, 且与服务器端内容一致") return file_name elif is_overwritable(file_name): with open(file_name, "rb") as f: for index, block_dict in enumerate(meta_dict['block']): f.seek(block_offset(index)) if calc_sha1(f.read(block_dict['size']), hexdigest=True) == block_dict['sha1']: - # log(f"分块{index} ({block_dict['size'] / 1024 / 1024:.2f} MB) 已存在于本地") + # log(f"分块{index + 1}/{len(meta_dict['block'])}校验通过") pass else: - # log(f"分块{index} ({block_dict['size'] / 1024 / 1024:.2f} MB) 需要重新下载") + # log(f"分块{index + 1}/{len(meta_dict['block'])}校验未通过") download_block_list.append(index) - log(f"{len(download_block_list)}个分块待下载") + log(f"{len(download_block_list)}/{len(meta_dict['block'])}个分块待下载") else: return None else: @@ -364,18 +366,18 @@ def download_handle(args): if terminate_flag.is_set(): return None f.truncate(sum(block['size'] for block in meta_dict['block'])) - log(f"{os.path.basename(file_name)}下载完毕, 用时{int(time.time() - start_time)}秒, 平均速度{meta_dict['size'] / 1024 / 1024 / (time.time() - start_time):.2f} MB/s") + log(f"{os.path.basename(file_name)} ({size_string(meta_dict['size'])}) 下载完毕, 用时{time.time() - start_time:.1f}秒, 平均速度{size_string(meta_dict['size'] / (time.time() - start_time))}/s") sha1 = calc_sha1(read_in_chunk(file_name), hexdigest=True) if sha1 == meta_dict['sha1']: - log(f"{os.path.basename(file_name)}校验通过") + log("文件校验通过") return file_name else: - log(f"{os.path.basename(file_name)}校验未通过") + log("文件校验未通过") return None if __name__ == "__main__": signal.signal(signal.SIGINT, lambda signum, frame: os.kill(os.getpid(), 9)) - parser = argparse.ArgumentParser(description="BiliDrive", epilog="By Hsury, 2019/11/30") + parser = argparse.ArgumentParser(description="BiliDrive", epilog="By Hsury, 2019/12/12") subparsers = parser.add_subparsers() history_parser = subparsers.add_parser("history", help="view upload history") history_parser.set_defaults(func=history_handle) -- GitLab