#!/usr/bin/env python3.7 # -*- coding: utf-8 -*- import argparse import hashlib import json import math import os import re import requests import signal import struct import sys import threading import time import types from bilibili import Bilibili bundle_dir = os.path.dirname(sys.executable) if getattr(sys, "frozen", False) else os.path.dirname(os.path.abspath(__file__)) 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 def bmp_header(data): return b"BM" \ + struct.pack("= args.thread: done_flag.acquire() if not terminate_flag.is_set(): thread_pool.append(threading.Thread(target=core, args=(index, block))) thread_pool[-1].start() else: log("已终止上传, 等待线程回收") for thread in thread_pool: thread.join() if terminate_flag.is_set(): return None sha1 = calc_sha1(read_in_chunks(file_name), hexdigest=True) meta_dict = { 'time': int(time.time()), 'filename': os.path.basename(file_name), 'size': os.path.getsize(file_name), 'sha1': sha1, 'block': [block_dict[i] for i in range(len(block_dict))], } meta = json.dumps(meta_dict, ensure_ascii=False).encode("utf-8") full_meta = bmp_header(meta) + meta for _ in range(10): response = image_upload(full_meta, cookies) 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)) write_history(first_4mb_sha1, meta_dict, url) return url log(f"元数据第{_ + 1}次上传失败") else: return None def download_handle(args): def core(index, block_dict): # log(f"分块{index} ({block_dict['size'] / 1024 / 1024:.2f} MB) 开始下载") for _ in range(10): block = image_download(block_dict['url'])[62:] if block: if calc_sha1(block, hexdigest=True) == block_dict['sha1']: file_lock.acquire() f.seek(block_offset(index)) f.write(block) file_lock.release() log(f"分块{index} ({block_dict['size'] / 1024 / 1024:.2f} MB) 下载完毕") done_flag.release() break else: log(f"分块{index} ({block_dict['size'] / 1024 / 1024:.2f} MB) 校验未通过") else: log(f"分块{index} ({block_dict['size'] / 1024 / 1024:.2f} MB) 第{_ + 1}次下载失败") else: terminate_flag.set() def block_offset(index): return sum(meta_dict['block'][i]['size'] for i in range(index)) def is_overwrite(file_name): if args.force: return True else: return (input(f"{os.path.basename(file_name)}已存在于本地, 是否覆盖? [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']))}") else: log("元数据解析失败") return None log(f"线程数: {args.thread}") download_block_list = [] if os.path.exists(file_name): if os.path.getsize(file_name) == meta_dict['size'] and calc_sha1(read_in_chunks(file_name), hexdigest=True) == meta_dict['sha1']: log(f"{os.path.basename(file_name)}已存在于本地, 且与服务器端文件内容一致") return file_name elif is_overwrite(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) 已存在于本地") pass else: # log(f"分块{index} ({block_dict['size'] / 1024 / 1024:.2f} MB) 需要重新下载") download_block_list.append(index) else: return None else: download_block_list = list(range(len(meta_dict['block']))) done_flag = threading.Semaphore(0) terminate_flag = threading.Event() file_lock = threading.Lock() thread_pool = [] with open(file_name, "r+b" if os.path.exists(file_name) else "wb") as f: for index in download_block_list: if len(thread_pool) >= args.thread: done_flag.acquire() if not terminate_flag.is_set(): thread_pool.append(threading.Thread(target=core, args=(index, meta_dict['block'][index]))) thread_pool[-1].start() else: log("已终止下载, 等待线程回收") for thread in thread_pool: thread.join() 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") sha1 = calc_sha1(read_in_chunks(file_name), hexdigest=True) if sha1 == meta_dict['sha1']: log(f"{os.path.basename(file_name)}校验通过") return file_name else: log(f"{os.path.basename(file_name)}校验未通过") return None if __name__ == "__main__": signal.signal(signal.SIGINT, lambda signum, frame: os.kill(os.getpid(), 9)) parser = argparse.ArgumentParser(description="Bilibili Drive", epilog="By Hsury, 2019/10/30") subparsers = parser.add_subparsers() history_parser = subparsers.add_parser("history", help="view upload history") history_parser.set_defaults(func=history_handle) info_parser = subparsers.add_parser("info", help="view meta info") info_parser.add_argument("meta", help="meta url") info_parser.set_defaults(func=info_handle) login_parser = subparsers.add_parser("login", help="log in to bilibili") login_parser.add_argument("username", help="username") login_parser.add_argument("password", help="password") login_parser.set_defaults(func=login_handle) upload_parser = subparsers.add_parser("upload", help="upload a file") upload_parser.add_argument("file", help="name of the file to upload") upload_parser.add_argument("-b", "--block-size", default=4, type=int, help="block size in MB") upload_parser.add_argument("-t", "--thread", default=4, type=int, help="upload thread number") upload_parser.set_defaults(func=upload_handle) download_parser = subparsers.add_parser("download", help="download a file") download_parser.add_argument("meta", help="meta url") download_parser.add_argument("file", nargs="?", default="", help="new file name") download_parser.add_argument("-f", "--force", action="store_true", help="force to overwrite if file exists") download_parser.add_argument("-t", "--thread", default=8, type=int, help="download thread number") download_parser.set_defaults(func=download_handle) shell = False while True: if shell: args = input("BiliDrive > ").split() if args == ["exit"]: break elif args == ["help"]: parser.print_help() else: try: args = parser.parse_args(args) args.func(args) except: pass else: args = parser.parse_args() try: args.func(args) break except AttributeError: shell = True parser.print_help()