__main__.py 13.8 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 shlex
H
Hsury 已提交
12
import signal
H
Hsury 已提交
13
import struct
H
Hsury 已提交
14
import sys
H
Hsury 已提交
15
import threading
H
Hsury 已提交
16
import time
H
Hsury 已提交
17
import traceback
H
Hsury 已提交
18
import types
W
wizardforcel 已提交
19 20 21
from BiliDriveEx import __version__
from BiliDriveEx.bilibili import Bilibili
from BiliDriveEx.encoder import Encoder
W
wizardforcel 已提交
22
from BiliDriveEx.util import *
H
Hsury 已提交
23

W
wizardforcel 已提交
24
encoder = Encoder()
W
wizardforcel 已提交
25
api = Bilibili()
H
Hsury 已提交
26

W
wizardforcel 已提交
27 28
def fetch_meta(s):
    if re.match(r"^bdex://[a-fA-F0-9]{40}$", s):
W
wizardforcel 已提交
29
        full_meta = image_download(api.default_url(re.findall(r"[a-fA-F0-9]{40}", s)[0]))
W
wizardforcel 已提交
30 31
    elif re.match(r"^bdrive://[a-fA-F0-9]{40}$", s):
        full_meta = image_download(
W
wizardforcel 已提交
32
            api.default_url(re.findall(r"[a-fA-F0-9]{40}", s)[0]).replace('png', 'x-ms-bmp')
W
wizardforcel 已提交
33 34 35
        )
    elif s.startswith("http://") or s.startswith("https://"):
        full_meta = image_download(s)
H
Hsury 已提交
36
    else:
W
wizardforcel 已提交
37
        return
H
Hsury 已提交
38
    try:
W
wizardforcel 已提交
39
        meta_dict = json.loads(encoder.decode(full_meta).decode("utf-8"))
H
Hsury 已提交
40 41
        return meta_dict
    except:
W
wizardforcel 已提交
42
        return
H
Hsury 已提交
43

H
Hsury 已提交
44

H
Hsury 已提交
45
def login_handle(args):
W
wizardforcel 已提交
46 47 48 49 50 51 52
    r = api.login(args.username, args.password)
    if r['code'] != 0:
        log(f"登录失败:{r['message']}")
        return
    info = api.get_user_info()
    if info: log(info)
    else: log("用户信息获取失败")
W
wizardforcel 已提交
53

W
wizardforcel 已提交
54 55 56
def cookies_handle(args):
    api.set_cookies(args.cookies)
    info = api.get_user_info()
W
wizardforcel 已提交
57
    if info: log(info)
W
wizardforcel 已提交
58
    else: log("用户信息获取失败")
H
Hsury 已提交
59

W
wizardforcel 已提交
60 61 62 63 64
def userinfo_handle(args):
    info = api.get_user_info()
    if info: log(info)
    else: log("用户未登录")

H
Hsury 已提交
65
def upload_handle(args):
H
Hsury 已提交
66
    def core(index, block):
H
Hsury 已提交
67
        try:
W
wizardforcel 已提交
68
            block_sha1 = calc_sha1(block)
W
wizardforcel 已提交
69
            full_block = encoder.encode(block)
W
wizardforcel 已提交
70
            full_block_sha1 = calc_sha1(full_block)
W
wizardforcel 已提交
71
            url = api.exist(full_block_sha1)
H
Hsury 已提交
72
            if url:
73
                log(f"分块{index + 1}/{block_num}上传完毕")
H
Hsury 已提交
74 75 76 77 78
                block_dict[index] = {
                    'url': url,
                    'size': len(block),
                    'sha1': block_sha1,
                }
H
Hsury 已提交
79
            else:
80
                # log(f"分块{index + 1}/{block_num}开始上传")
H
Hsury 已提交
81
                for _ in range(10):
H
Hsury 已提交
82 83
                    if terminate_flag.is_set():
                        return
W
wizardforcel 已提交
84
                    response = api.image_upload(full_block)
H
Hsury 已提交
85 86 87
                    if response:
                        if response['code'] == 0:
                            url = response['data']['image_url']
88
                            log(f"分块{index + 1}/{block_num}上传完毕")
H
Hsury 已提交
89 90 91 92 93
                            block_dict[index] = {
                                'url': url,
                                'size': len(block),
                                'sha1': block_sha1,
                            }
H
Hsury 已提交
94
                            return
H
Hsury 已提交
95 96
                        elif response['code'] == -4:
                            terminate_flag.set()
97
                            log(f"分块{index + 1}/{block_num}{_ + 1}次上传失败, 请重新登录")
H
Hsury 已提交
98
                            return
99
                    log(f"分块{index + 1}/{block_num}{_ + 1}次上传失败")
H
Hsury 已提交
100 101 102 103 104
                else:
                    terminate_flag.set()
        except:
            terminate_flag.set()
            traceback.print_exc()
H
Hsury 已提交
105 106
        finally:
            done_flag.release()
H
Hsury 已提交
107

H
Hsury 已提交
108 109
    start_time = time.time()
    file_name = args.file
H
Hsury 已提交
110
    if not os.path.exists(file_name):
111
        log(f"文件{file_name}不存在")
W
wizardforcel 已提交
112
        return
H
Hsury 已提交
113
    if os.path.isdir(file_name):
114
        log("暂不支持上传文件夹")
W
wizardforcel 已提交
115
        return
116
    log(f"上传: {os.path.basename(file_name)} ({size_string(os.path.getsize(file_name))})")
W
wizardforcel 已提交
117
    first_4mb_sha1 = calc_sha1(read_in_chunk(file_name, size=4 * 1024 * 1024, cnt=1))
H
Hsury 已提交
118
    history = read_history()
H
Hsury 已提交
119 120
    if first_4mb_sha1 in history:
        url = history[first_4mb_sha1]['url']
121
        log(f"文件已于{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(history[first_4mb_sha1]['time']))}上传, 共有{len(history[first_4mb_sha1]['block'])}个分块")
W
wizardforcel 已提交
122
        log(f"META URL -> {api.meta_string(url)}")
H
Hsury 已提交
123
        return url
W
wizardforcel 已提交
124

W
wizardforcel 已提交
125 126 127 128
    if not api.get_user_info():
        log("账号未登录,请先登录")
        return
        
H
Hsury 已提交
129
    log(f"线程数: {args.thread}")
H
Hsury 已提交
130 131 132 133
    done_flag = threading.Semaphore(0)
    terminate_flag = threading.Event()
    thread_pool = []
    block_dict = {}
134
    block_num = math.ceil(os.path.getsize(file_name) / (args.block_size * 1024 * 1024))
W
wizardforcel 已提交
135
    for index, block in enumerate(read_in_chunk(file_name, size=args.block_size * 1024 * 1024)):
H
Hsury 已提交
136 137 138 139 140
        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 已提交
141
        else:
H
Hsury 已提交
142
            log("已终止上传, 等待线程回收")
H
Hsury 已提交
143
            break
H
Hsury 已提交
144 145 146
    for thread in thread_pool:
        thread.join()
    if terminate_flag.is_set():
W
wizardforcel 已提交
147
        return
W
wizardforcel 已提交
148
    sha1 = calc_sha1(read_in_chunk(file_name))
H
Hsury 已提交
149
    meta_dict = {
H
Hsury 已提交
150
        'time': int(time.time()),
H
Hsury 已提交
151
        'filename': os.path.basename(file_name),
H
Hsury 已提交
152
        'size': os.path.getsize(file_name),
H
Hsury 已提交
153 154
        'sha1': sha1,
        'block': [block_dict[i] for i in range(len(block_dict))],
H
Hsury 已提交
155
    }
H
Hsury 已提交
156
    meta = json.dumps(meta_dict, ensure_ascii=False).encode("utf-8")
W
wizardforcel 已提交
157
    full_meta = encoder.encode(meta)
H
Hsury 已提交
158
    for _ in range(10):
W
wizardforcel 已提交
159
        response = api.image_upload(full_meta)
H
Hsury 已提交
160
        if response and response['code'] == 0:
H
Hsury 已提交
161
            url = response['data']['image_url']
H
Hsury 已提交
162
            log("元数据上传完毕")
163
            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")
W
wizardforcel 已提交
164
            log(f"META URL -> {api.meta_string(url)}")
H
Hsury 已提交
165
            write_history(first_4mb_sha1, meta_dict, url)
H
Hsury 已提交
166
            return url
H
Hsury 已提交
167
        log(f"元数据第{_ + 1}次上传失败")
H
Hsury 已提交
168
    else:
W
wizardforcel 已提交
169
        return
H
Hsury 已提交
170 171

def download_handle(args):
H
Hsury 已提交
172
    def core(index, block_dict):
H
Hsury 已提交
173
        try:
174
            # log(f"分块{index + 1}/{len(meta_dict['block'])}开始下载")
H
Hsury 已提交
175
            for _ in range(10):
H
Hsury 已提交
176 177
                if terminate_flag.is_set():
                    return
H
Hsury 已提交
178 179
                block = image_download(block_dict['url'])
                if block:
W
wizardforcel 已提交
180
                    block = encoder.decode(block)
W
wizardforcel 已提交
181
                    if calc_sha1(block) == block_dict['sha1']:
H
Hsury 已提交
182 183 184 185
                        file_lock.acquire()
                        f.seek(block_offset(index))
                        f.write(block)
                        file_lock.release()
186
                        log(f"分块{index + 1}/{len(meta_dict['block'])}下载完毕")
H
Hsury 已提交
187
                        return
H
Hsury 已提交
188
                    else:
189
                        log(f"分块{index + 1}/{len(meta_dict['block'])}校验未通过")
W
wizardforcel 已提交
190
                        terminate_flag.set()
H
Hsury 已提交
191
                else:
192
                    log(f"分块{index + 1}/{len(meta_dict['block'])}{_ + 1}次下载失败")
W
wizardforcel 已提交
193
                    terminate_flag.set()
H
Hsury 已提交
194
        except:
H
Hsury 已提交
195
            terminate_flag.set()
H
Hsury 已提交
196
            traceback.print_exc()
H
Hsury 已提交
197 198
        finally:
            done_flag.release()
H
Hsury 已提交
199 200 201

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

H
Hsury 已提交
203
    def is_overwritable(file_name):
H
Hsury 已提交
204 205 206
        if args.force:
            return True
        else:
207
            return (input("文件已存在, 是否覆盖? [y/N] ") in ["y", "Y"])
H
Hsury 已提交
208

H
Hsury 已提交
209
    start_time = time.time()
H
Hsury 已提交
210 211 212
    meta_dict = fetch_meta(args.meta)
    if meta_dict:
        file_name = args.file if args.file else meta_dict['filename']
213
        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']))}")
H
Hsury 已提交
214
    else:
H
Hsury 已提交
215
        log("元数据解析失败")
W
wizardforcel 已提交
216
        return
H
Hsury 已提交
217
    log(f"线程数: {args.thread}")
H
Hsury 已提交
218
    download_block_list = []
H
Hsury 已提交
219
    if os.path.exists(file_name):
W
wizardforcel 已提交
220
        if os.path.getsize(file_name) == meta_dict['size'] and calc_sha1(read_in_chunk(file_name)) == meta_dict['sha1']:
221
            log("文件已存在, 且与服务器端内容一致")
H
Hsury 已提交
222
            return file_name
H
Hsury 已提交
223
        elif is_overwritable(file_name):
H
Hsury 已提交
224 225 226
            with open(file_name, "rb") as f:
                for index, block_dict in enumerate(meta_dict['block']):
                    f.seek(block_offset(index))
W
wizardforcel 已提交
227
                    if calc_sha1(f.read(block_dict['size'])) == block_dict['sha1']:
228
                        # log(f"分块{index + 1}/{len(meta_dict['block'])}校验通过")
H
Hsury 已提交
229
                        pass
H
Hsury 已提交
230
                    else:
231
                        # log(f"分块{index + 1}/{len(meta_dict['block'])}校验未通过")
H
Hsury 已提交
232
                        download_block_list.append(index)
233
            log(f"{len(download_block_list)}/{len(meta_dict['block'])}个分块待下载")
H
Hsury 已提交
234
        else:
W
wizardforcel 已提交
235
            return
H
Hsury 已提交
236
    else:
H
Hsury 已提交
237
        download_block_list = list(range(len(meta_dict['block'])))
H
Hsury 已提交
238 239 240 241
    done_flag = threading.Semaphore(0)
    terminate_flag = threading.Event()
    file_lock = threading.Lock()
    thread_pool = []
H
Hsury 已提交
242 243
    with open(file_name, "r+b" if os.path.exists(file_name) else "wb") as f:
        for index in download_block_list:
H
Hsury 已提交
244 245 246
            if len(thread_pool) >= args.thread:
                done_flag.acquire()
            if not terminate_flag.is_set():
H
Hsury 已提交
247
                thread_pool.append(threading.Thread(target=core, args=(index, meta_dict['block'][index])))
H
Hsury 已提交
248
                thread_pool[-1].start()
H
Hsury 已提交
249
            else:
H
Hsury 已提交
250
                log("已终止下载, 等待线程回收")
H
Hsury 已提交
251
                break
H
Hsury 已提交
252 253 254
        for thread in thread_pool:
            thread.join()
        if terminate_flag.is_set():
W
wizardforcel 已提交
255
            return
H
Hsury 已提交
256
        f.truncate(sum(block['size'] for block in meta_dict['block']))
257
    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")
W
wizardforcel 已提交
258
    sha1 = calc_sha1(read_in_chunk(file_name))
H
Hsury 已提交
259
    if sha1 == meta_dict['sha1']:
260
        log("文件校验通过")
H
Hsury 已提交
261
        return file_name
H
Hsury 已提交
262
    else:
263
        log("文件校验未通过")
W
wizardforcel 已提交
264
        return
H
Hsury 已提交
265

H
Hsury 已提交
266 267 268
def info_handle(args):
    meta_dict = fetch_meta(args.meta)
    if meta_dict:
W
wizardforcel 已提交
269
        print_meta(meta_dict)
H
Hsury 已提交
270 271 272 273 274 275 276 277 278
    else:
        print("元数据解析失败")

def history_handle(args):
    history = read_history()
    if history:
        for index, meta_dict in enumerate(history.values()):
            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']))}")
W
wizardforcel 已提交
279
            print(f"{' ' * len(prefix)} META URL -> {api.meta_string(meta_dict['url'])}")
H
Hsury 已提交
280 281 282 283
    else:
        print(f"暂无历史记录")

def main():
H
Hsury 已提交
284
    signal.signal(signal.SIGINT, lambda signum, frame: os.kill(os.getpid(), 9))
W
wizardforcel 已提交
285 286
    parser = argparse.ArgumentParser(prog="BiliDriveEx", description="Make Bilibili A Great Cloud Storage!", formatter_class=argparse.RawDescriptionHelpFormatter)
    parser.add_argument("-v", "--version", action="version", version=f"BiliDriveEx version: {__version__}")
H
Hsury 已提交
287
    subparsers = parser.add_subparsers()
W
wizardforcel 已提交
288
    
H
Hsury 已提交
289
    login_parser = subparsers.add_parser("login", help="log in to bilibili")
H
Hsury 已提交
290 291
    login_parser.add_argument("username", help="your bilibili username")
    login_parser.add_argument("password", help="your bilibili password")
H
Hsury 已提交
292
    login_parser.set_defaults(func=login_handle)
W
wizardforcel 已提交
293 294 295 296 297
    
    cookies_parser = subparsers.add_parser("cookies", help="set cookies to bilibili")
    cookies_parser.add_argument("cookies", help="your bilibili cookies")
    cookies_parser.set_defaults(func=cookies_handle)

W
wizardforcel 已提交
298 299 300
    userinfo_parser = subparsers.add_parser("userinfo", help="get userinfo")
    userinfo_parser.set_defaults(func=userinfo_handle)
    
H
Hsury 已提交
301
    upload_parser = subparsers.add_parser("upload", help="upload a file")
H
Hsury 已提交
302
    upload_parser.add_argument("file", help="name of the file to upload")
H
Hsury 已提交
303
    upload_parser.add_argument("-b", "--block-size", default=4, type=int, help="block size in MB")
H
Hsury 已提交
304
    upload_parser.add_argument("-t", "--thread", default=4, type=int, help="upload thread number")
H
Hsury 已提交
305
    upload_parser.set_defaults(func=upload_handle)
W
wizardforcel 已提交
306
    
H
Hsury 已提交
307
    download_parser = subparsers.add_parser("download", help="download a file")
H
Hsury 已提交
308
    download_parser.add_argument("meta", help="meta url")
H
Hsury 已提交
309
    download_parser.add_argument("file", nargs="?", default="", help="new file name")
H
Hsury 已提交
310
    download_parser.add_argument("-f", "--force", action="store_true", help="force to overwrite if file exists")
H
Hsury 已提交
311
    download_parser.add_argument("-t", "--thread", default=8, type=int, help="download thread number")
H
Hsury 已提交
312
    download_parser.set_defaults(func=download_handle)
W
wizardforcel 已提交
313
    
H
Hsury 已提交
314 315 316 317 318
    info_parser = subparsers.add_parser("info", help="show meta info")
    info_parser.add_argument("meta", help="meta url")
    info_parser.set_defaults(func=info_handle)
    history_parser = subparsers.add_parser("history", help="show upload history")
    history_parser.set_defaults(func=history_handle)
H
Hsury 已提交
319 320 321
    shell = False
    while True:
        if shell:
W
wizardforcel 已提交
322
            args = shlex.split(input("BiliDriveEx > "))
H
Hsury 已提交
323 324 325 326 327
            try:
                args = parser.parse_args(args)
                args.func(args)
            except:
                pass
H
Hsury 已提交
328 329 330 331
        else:
            args = parser.parse_args()
            try:
                args.func(args)
H
Hsury 已提交
332
                break
W
wizardforcel 已提交
333 334
            except AttributeError as ex:
                traceback.print_exc(file=sys.stdout)
H
Hsury 已提交
335
                shell = True
H
Hsury 已提交
336 337 338 339 340 341 342
                subparsers.add_parser("help", help="show this help message").set_defaults(func=lambda _: parser.parse_args(["--help"]).func())
                subparsers.add_parser("version", help="show program's version number").set_defaults(func=lambda _: parser.parse_args(["--version"]).func())
                subparsers.add_parser("exit", help="exit program").set_defaults(func=lambda _: os._exit(0))
                parser.print_help()

if __name__ == "__main__":
    main()