drive.py 15.7 KB
Newer Older
H
Hsury 已提交
1 2 3 4 5 6 7 8
#!/usr/bin/env python3.7
# -*- coding: utf-8 -*-

import argparse
import hashlib
import json
import math
import os
H
Hsury 已提交
9
import re
H
Hsury 已提交
10
import requests
H
Hsury 已提交
11
import signal
H
Hsury 已提交
12
import struct
H
Hsury 已提交
13
import sys
H
Hsury 已提交
14
import threading
H
Hsury 已提交
15 16 17 18
import time
import types
from bilibili import Bilibili

H
Hsury 已提交
19 20
bundle_dir = os.path.dirname(sys.executable) if getattr(sys, "frozen", False) else os.path.dirname(os.path.abspath(__file__))

H
Hsury 已提交
21 22
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
H
Hsury 已提交
23

H
Hsury 已提交
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
def bmp_header(data):
    return b"BM" \
        + struct.pack("<l", 14 + 40 + 8 + len(data)) \
        + b"\x00\x00" \
        + b"\x00\x00" \
        + b"\x3e\x00\x00\x00" \
        + b"\x28\x00\x00\x00" \
        + struct.pack("<l", len(data)) \
        + b"\x01\x00\x00\x00" \
        + b"\x01\x00" \
        + b"\x01\x00" \
        + b"\x00\x00\x00\x00" \
        + struct.pack("<l", math.ceil(len(data) / 8)) \
        + b"\x00\x00\x00\x00" \
        + b"\x00\x00\x00\x00" \
        + b"\x00\x00\x00\x00" \
        + b"\x00\x00\x00\x00" \
        + b"\x00\x00\x00\x00\xff\xff\xff\x00"
H
Hsury 已提交
42

H
Hsury 已提交
43 44 45 46 47 48 49 50 51 52
def calc_sha1(data, hexdigest=False):
    sha1 = hashlib.sha1()
    if isinstance(data, types.GeneratorType):
        for chunk in data:
            sha1.update(chunk)
    else:
        sha1.update(data)
    return sha1.hexdigest() if hexdigest else sha1.digest()

def fetch_meta(string):
H
Hsury 已提交
53 54
    if re.match(r"^bdrive://[a-fA-F0-9]{40}$", string) or re.match(r"^[a-fA-F0-9]{40}$", string):
        full_meta = image_download(default_url(re.findall(r"[a-fA-F0-9]{40}", string)[0]))
H
Hsury 已提交
55 56 57 58 59 60 61 62 63 64
    elif string.startswith("http://") or string.startswith("https://"):
        full_meta = image_download(string)
    else:
        return None
    try:
        meta_dict = json.loads(full_meta[62:].decode("utf-8"))
        return meta_dict
    except:
        return None

H
Hsury 已提交
65
def image_upload(data, cookies):
H
Hsury 已提交
66 67 68 69 70 71 72
    url = "https://api.vc.bilibili.com/api/v1/drawImage/upload"
    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/77.0.3865.120 Safari/537.36",
    }
    files = {
H
Hsury 已提交
73
        'file_up': (f"{int(time.time() * 1000)}.bmp", data),
H
Hsury 已提交
74 75 76
        'biz': "draw",
        'category': "daily",
    }
H
Hsury 已提交
77 78 79 80
    try:
        response = requests.post(url, headers=headers, cookies=cookies, files=files).json()
    except:
        response = None
H
Hsury 已提交
81 82
    return response

H
Hsury 已提交
83
def image_download(url):
H
Hsury 已提交
84 85 86 87 88
    try:
        response = requests.get(url).content
    except:
        response = None
    return response
H
Hsury 已提交
89

H
Hsury 已提交
90
def log(message):
H
Hsury 已提交
91
    Bilibili._log(message)
H
Hsury 已提交
92 93

def read_history():
H
Hsury 已提交
94
    try:
H
Hsury 已提交
95
        with open(os.path.join(bundle_dir, "history.json"), "r", encoding="utf-8") as f:
H
Hsury 已提交
96
            history = json.loads(f.read())
H
Hsury 已提交
97
    except:
H
Hsury 已提交
98 99
        history = {}
    return history
H
Hsury 已提交
100

H
Hsury 已提交
101 102
def read_in_chunks(file_name, chunk_size=16 * 1024 * 1024, chunk_number=-1):
    chunk_counter = 0
H
Hsury 已提交
103 104 105
    with open(file_name, "rb") as f:
        while True:
            data = f.read(chunk_size)
H
Hsury 已提交
106
            if data != b"" and (chunk_number == -1 or chunk_counter < chunk_number):
H
Hsury 已提交
107
                yield data
H
Hsury 已提交
108
                chunk_counter += 1
H
Hsury 已提交
109 110 111 112 113 114 115 116 117 118 119
            else:
                return

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'])}")
    else:
H
Hsury 已提交
120
        print(f"暂无历史记录")
H
Hsury 已提交
121 122

def info_handle(args):
H
Hsury 已提交
123 124
    meta_dict = fetch_meta(args.meta)
    if meta_dict:
H
Hsury 已提交
125 126 127 128 129
        print(f"文件名: {meta_dict['filename']}")
        print(f"大小: {meta_dict['size'] / 1024 / 1024:.2f} MB")
        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'])}")
H
Hsury 已提交
130
        for index, block_dict in enumerate(meta_dict['block']):
H
Hsury 已提交
131
            print(f"分块{index} ({block_dict['size'] / 1024 / 1024:.2f} MB) URL: {block_dict['url']}")
H
Hsury 已提交
132
    else:
H
Hsury 已提交
133
        print("元数据解析失败")
H
Hsury 已提交
134

H
Hsury 已提交
135 136 137 138
def login_handle(args):
    bilibili = Bilibili()
    if bilibili.login(username=args.username, password=args.password):
        bilibili.get_user_info()
H
Hsury 已提交
139
        with open(os.path.join(bundle_dir, "cookies.json"), "w", encoding="utf-8") as f:
H
Hsury 已提交
140 141
            f.write(json.dumps(bilibili.get_cookies(), ensure_ascii=False, indent=2))

H
Hsury 已提交
142
def upload_handle(args):
H
Hsury 已提交
143
    def core(index, block):
H
Hsury 已提交
144 145 146 147
        block_sha1 = calc_sha1(block, hexdigest=True)
        full_block = bmp_header(block) + block
        full_block_sha1 = calc_sha1(full_block, hexdigest=True)
        url = skippable(full_block_sha1)
H
Hsury 已提交
148
        if url:
H
Hsury 已提交
149
            # log(f"分块{index} ({len(block) / 1024 / 1024:.2f} MB) 已存在于服务器")
H
Hsury 已提交
150 151
            block_dict[index] = {
                'url': url,
H
Hsury 已提交
152
                'size': len(block),
H
Hsury 已提交
153 154 155 156
                'sha1': block_sha1,
            }
            done_flag.release()
        else:
H
Hsury 已提交
157 158
            # log(f"分块{index} ({len(block) / 1024 / 1024:.2f} MB) 开始上传")
            for _ in range(10):
H
Hsury 已提交
159
                response = image_upload(full_block, cookies)
H
Hsury 已提交
160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175
                if response:
                    if response['code'] == 0:
                        url = response['data']['image_url']
                        log(f"分块{index} ({len(block) / 1024 / 1024:.2f} MB) 上传完毕")
                        block_dict[index] = {
                            'url': url,
                            'size': len(block),
                            'sha1': block_sha1,
                        }
                        done_flag.release()
                        break
                    elif response['code'] == -4:
                        terminate_flag.set()
                        log(f"分块{index} ({len(block) / 1024 / 1024:.2f} MB) 第{_ + 1}次上传失败, 请重新登录")
                        break
                log(f"分块{index} ({len(block) / 1024 / 1024:.2f} MB) 第{_ + 1}次上传失败")
H
Hsury 已提交
176 177 178 179
            else:
                terminate_flag.set()

    def skippable(sha1):
H
Hsury 已提交
180
        url = default_url(sha1)
H
Hsury 已提交
181 182 183
        response = requests.head(url)
        return url if response.status_code == 200 else None

H
Hsury 已提交
184
    def write_history(first_4mb_sha1, meta_dict, url):
H
Hsury 已提交
185
        history = read_history()
H
Hsury 已提交
186 187
        history[first_4mb_sha1] = meta_dict
        history[first_4mb_sha1]['url'] = url
H
Hsury 已提交
188
        with open(os.path.join(bundle_dir, "history.json"), "w", encoding="utf-8") as f:
H
Hsury 已提交
189 190
            f.write(json.dumps(history, ensure_ascii=False, indent=2))

H
Hsury 已提交
191 192
    start_time = time.time()
    try:
H
Hsury 已提交
193
        with open(os.path.join(bundle_dir, "cookies.json"), "r", encoding="utf-8") as f:
H
Hsury 已提交
194 195
            cookies = json.loads(f.read())
    except:
H
Hsury 已提交
196
        log("Cookies加载失败, 请先登录")
H
Hsury 已提交
197 198
        return None
    file_name = args.file
H
Hsury 已提交
199 200 201
    if not os.path.exists(file_name):
        log(f"{file_name}不存在")
        return None
H
Hsury 已提交
202
    log(f"上传: {os.path.basename(file_name)} ({os.path.getsize(file_name) / 1024 / 1024:.2f} MB)")
H
Hsury 已提交
203
    first_4mb_sha1 = calc_sha1(read_in_chunks(file_name, chunk_size=4 * 1024 * 1024, chunk_number=1), hexdigest=True)
H
Hsury 已提交
204
    history = read_history()
H
Hsury 已提交
205 206 207
    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'])}个分块")
H
Hsury 已提交
208 209
        log(meta_string(url))
        return url
H
Hsury 已提交
210
    log(f"线程数: {args.thread}")
H
Hsury 已提交
211 212 213 214
    done_flag = threading.Semaphore(0)
    terminate_flag = threading.Event()
    thread_pool = []
    block_dict = {}
H
Hsury 已提交
215
    for index, block in enumerate(read_in_chunks(file_name, chunk_size=args.block_size * 1024 * 1024)):
H
Hsury 已提交
216 217 218 219 220
        if len(thread_pool) >= 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()
H
Hsury 已提交
221
        else:
H
Hsury 已提交
222 223 224 225 226
            log("已终止上传, 等待线程回收")
    for thread in thread_pool:
        thread.join()
    if terminate_flag.is_set():
        return None
H
Hsury 已提交
227
    sha1 = calc_sha1(read_in_chunks(file_name), hexdigest=True)
H
Hsury 已提交
228
    meta_dict = {
H
Hsury 已提交
229
        'time': int(time.time()),
H
Hsury 已提交
230
        'filename': os.path.basename(file_name),
H
Hsury 已提交
231
        'size': os.path.getsize(file_name),
H
Hsury 已提交
232 233
        'sha1': sha1,
        'block': [block_dict[i] for i in range(len(block_dict))],
H
Hsury 已提交
234
    }
H
Hsury 已提交
235 236
    meta = json.dumps(meta_dict, ensure_ascii=False).encode("utf-8")
    full_meta = bmp_header(meta) + meta
H
Hsury 已提交
237
    for _ in range(10):
H
Hsury 已提交
238
        response = image_upload(full_meta, cookies)
H
Hsury 已提交
239
        if response and response['code'] == 0:
H
Hsury 已提交
240
            url = response['data']['image_url']
H
Hsury 已提交
241
            log("元数据上传完毕")
H
Hsury 已提交
242
            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")
H
Hsury 已提交
243
            log(meta_string(url))
H
Hsury 已提交
244
            write_history(first_4mb_sha1, meta_dict, url)
H
Hsury 已提交
245
            return url
H
Hsury 已提交
246
        log(f"元数据第{_ + 1}次上传失败")
H
Hsury 已提交
247
    else:
H
Hsury 已提交
248
        return None
H
Hsury 已提交
249 250

def download_handle(args):
H
Hsury 已提交
251 252 253
    def core(index, block_dict):
        # log(f"分块{index} ({block_dict['size'] / 1024 / 1024:.2f} MB) 开始下载")
        for _ in range(10):
H
Hsury 已提交
254
            block = image_download(block_dict['url'])[62:]
H
Hsury 已提交
255 256 257 258 259 260 261 262 263 264
            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:
H
Hsury 已提交
265
                    log(f"分块{index} ({block_dict['size'] / 1024 / 1024:.2f} MB) 校验未通过")
H
Hsury 已提交
266 267
            else:
                log(f"分块{index} ({block_dict['size'] / 1024 / 1024:.2f} MB) 第{_ + 1}次下载失败")
H
Hsury 已提交
268
        else:
H
Hsury 已提交
269 270 271 272
            terminate_flag.set()

    def block_offset(index):
        return sum(meta_dict['block'][i]['size'] for i in range(index))
H
Hsury 已提交
273

H
Hsury 已提交
274 275 276 277 278 279
    def is_overwrite(file_name):
        if args.force:
            return True
        else:
            return (input(f"{os.path.basename(file_name)}已存在于本地, 是否覆盖? [y/N] ") in ["y", "Y"])

H
Hsury 已提交
280
    start_time = time.time()
H
Hsury 已提交
281 282 283
    meta_dict = fetch_meta(args.meta)
    if meta_dict:
        file_name = args.file if args.file else meta_dict['filename']
H
Hsury 已提交
284
        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']))}")
H
Hsury 已提交
285
    else:
H
Hsury 已提交
286
        log("元数据解析失败")
H
Hsury 已提交
287
        return None
H
Hsury 已提交
288
    log(f"线程数: {args.thread}")
H
Hsury 已提交
289
    download_block_list = []
H
Hsury 已提交
290 291 292
    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)}已存在于本地, 且与服务器端文件内容一致")
H
Hsury 已提交
293
            return file_name
H
Hsury 已提交
294
        elif is_overwrite(file_name):
H
Hsury 已提交
295 296 297 298
            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']:
H
Hsury 已提交
299 300
                        # log(f"分块{index} ({block_dict['size'] / 1024 / 1024:.2f} MB) 已存在于本地")
                        pass
H
Hsury 已提交
301
                    else:
H
Hsury 已提交
302
                        # log(f"分块{index} ({block_dict['size'] / 1024 / 1024:.2f} MB) 需要重新下载")
H
Hsury 已提交
303
                        download_block_list.append(index)
H
Hsury 已提交
304 305
        else:
            return None
H
Hsury 已提交
306 307
    else:
        download_block_list = list(range(len(meta_dict['block'])))
H
Hsury 已提交
308 309 310 311
    done_flag = threading.Semaphore(0)
    terminate_flag = threading.Event()
    file_lock = threading.Lock()
    thread_pool = []
H
Hsury 已提交
312 313
    with open(file_name, "r+b" if os.path.exists(file_name) else "wb") as f:
        for index in download_block_list:
H
Hsury 已提交
314 315 316
            if len(thread_pool) >= args.thread:
                done_flag.acquire()
            if not terminate_flag.is_set():
H
Hsury 已提交
317
                thread_pool.append(threading.Thread(target=core, args=(index, meta_dict['block'][index])))
H
Hsury 已提交
318
                thread_pool[-1].start()
H
Hsury 已提交
319
            else:
H
Hsury 已提交
320 321 322 323 324
                log("已终止下载, 等待线程回收")
        for thread in thread_pool:
            thread.join()
        if terminate_flag.is_set():
            return None
H
Hsury 已提交
325
        f.truncate(sum(block['size'] for block in meta_dict['block']))
H
Hsury 已提交
326
    log(f"{os.path.basename(file_name)}下载完毕, 用时{int(time.time() - start_time)}秒, 平均速度{meta_dict['size'] / 1024 / 1024 / (time.time() - start_time):.2f} MB/s")
H
Hsury 已提交
327 328
    sha1 = calc_sha1(read_in_chunks(file_name), hexdigest=True)
    if sha1 == meta_dict['sha1']:
H
Hsury 已提交
329
        log(f"{os.path.basename(file_name)}校验通过")
H
Hsury 已提交
330
        return file_name
H
Hsury 已提交
331
    else:
H
Hsury 已提交
332
        log(f"{os.path.basename(file_name)}校验未通过")
H
Hsury 已提交
333
        return None
H
Hsury 已提交
334 335

if __name__ == "__main__":
H
Hsury 已提交
336 337
    signal.signal(signal.SIGINT, lambda signum, frame: os.kill(os.getpid(), 9))
    parser = argparse.ArgumentParser(description="Bilibili Drive", epilog="By Hsury, 2019/10/30")
H
Hsury 已提交
338
    subparsers = parser.add_subparsers()
H
Hsury 已提交
339 340 341 342 343
    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)
H
Hsury 已提交
344
    login_parser = subparsers.add_parser("login", help="log in to bilibili")
H
Hsury 已提交
345 346 347 348
    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")
H
Hsury 已提交
349
    upload_parser.add_argument("file", help="name of the file to upload")
H
Hsury 已提交
350
    upload_parser.add_argument("-b", "--block-size", default=4, type=int, help="block size in MB")
H
Hsury 已提交
351
    upload_parser.add_argument("-t", "--thread", default=4, type=int, help="upload thread number")
H
Hsury 已提交
352 353
    upload_parser.set_defaults(func=upload_handle)
    download_parser = subparsers.add_parser("download", help="download a file")
H
Hsury 已提交
354
    download_parser.add_argument("meta", help="meta url")
H
Hsury 已提交
355
    download_parser.add_argument("file", nargs="?", default="", help="new file name")
H
Hsury 已提交
356
    download_parser.add_argument("-f", "--force", action="store_true", help="force to overwrite if file exists")
H
Hsury 已提交
357
    download_parser.add_argument("-t", "--thread", default=8, type=int, help="download thread number")
H
Hsury 已提交
358
    download_parser.set_defaults(func=download_handle)
H
Hsury 已提交
359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376
    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)
H
Hsury 已提交
377
                break
H
Hsury 已提交
378 379 380
            except AttributeError:
                shell = True
                parser.print_help()