__main__.py 10.5 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
from concurrent.futures import ThreadPoolExecutor
W
wizardforcel 已提交
20 21 22
from BiliDriveEx import __version__
from BiliDriveEx.bilibili import Bilibili
from BiliDriveEx.encoder import Encoder
W
wizardforcel 已提交
23
from BiliDriveEx.util import *
H
Hsury 已提交
24

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

W
wizardforcel 已提交
28 29 30 31
succ = True
nblocks = 0
lock = threading.Lock()

W
wizardforcel 已提交
32
def fetch_meta(s):
W
wizardforcel 已提交
33 34 35 36 37 38
    url = api.meta2real(s)
    if not url: return None
    full_meta = image_download(url)
    if not full_meta: return None
    meta_dict = json.loads(encoder.decode(full_meta).decode("utf-8"))
    return meta_dict
H
Hsury 已提交
39

H
Hsury 已提交
40
def login_handle(args):
W
wizardforcel 已提交
41 42 43 44 45 46 47
    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 已提交
48

W
wizardforcel 已提交
49 50 51
def cookies_handle(args):
    api.set_cookies(args.cookies)
    info = api.get_user_info()
W
wizardforcel 已提交
52
    if info: log(info)
W
wizardforcel 已提交
53
    else: log("用户信息获取失败")
H
Hsury 已提交
54

W
wizardforcel 已提交
55 56 57 58
def userinfo_handle(args):
    info = api.get_user_info()
    if info: log(info)
    else: log("用户未登录")
W
wizardforcel 已提交
59 60 61
    
def tr_upload(i, block, block_dict):
    global succ
W
wizardforcel 已提交
62

W
wizardforcel 已提交
63
    enco_block = encoder.encode(block)
W
wizardforcel 已提交
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
    for j in range(10):
        if not succ: break
        r = api.image_upload(enco_block)
        if r['code'] == 0:
            url = r['data']['image_url']
            with lock: 
                block_dict.update({
                    'url': url,
                    'size': len(block),
                    'sha1': calc_sha1(block),
                })
            log(f'分块{i + 1}/{nblocks}上传完毕')
            break
        else:
            log(f"分块{i + 1}/{nblocks}{j + 1}次上传失败:{r.get('message')}")
            if j == 9: succ = False
W
wizardforcel 已提交
80
    
H
Hsury 已提交
81
def upload_handle(args):
W
wizardforcel 已提交
82 83
    global succ
    global nblocks
H
Hsury 已提交
84

H
Hsury 已提交
85 86
    start_time = time.time()
    file_name = args.file
H
Hsury 已提交
87
    if not os.path.exists(file_name):
88
        log(f"文件{file_name}不存在")
W
wizardforcel 已提交
89
        return
H
Hsury 已提交
90
    if os.path.isdir(file_name):
91
        log("暂不支持上传文件夹")
W
wizardforcel 已提交
92
        return
93
    log(f"上传: {os.path.basename(file_name)} ({size_string(os.path.getsize(file_name))})")
W
wizardforcel 已提交
94
    first_4mb_sha1 = calc_sha1(read_in_chunk(file_name, size=4 * 1024 * 1024, cnt=1))
H
Hsury 已提交
95
    history = read_history()
H
Hsury 已提交
96 97
    if first_4mb_sha1 in history:
        url = history[first_4mb_sha1]['url']
98
        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 已提交
99
        log(f"META URL -> {api.real2meta(url)}")
H
Hsury 已提交
100
        return url
W
wizardforcel 已提交
101

W
wizardforcel 已提交
102 103 104 105
    if not api.get_user_info():
        log("账号未登录,请先登录")
        return
        
H
Hsury 已提交
106
    log(f"线程数: {args.thread}")
W
wizardforcel 已提交
107 108 109 110 111 112 113 114 115 116 117 118 119
    succ = True
    nblocks = math.ceil(os.path.getsize(file_name) / (args.block_size * 1024 * 1024))
    block_dicts = [{} for _ in range(nblocks)]
    trpool = ThreadPoolExecutor(args.thread)
    hdls = []
    
    blocks = read_in_chunk(file_name, size=args.block_size * 1024 * 1024)
    for i, block in enumerate(blocks):
        hdl = trpool.submit(tr_upload, i, block, block_dicts[i])
        hdls.append(hdl)
    for h in hdls: h.result()
    if not succ: return
    
W
wizardforcel 已提交
120
    sha1 = calc_sha1(read_in_chunk(file_name))
H
Hsury 已提交
121
    meta_dict = {
H
Hsury 已提交
122
        'time': int(time.time()),
H
Hsury 已提交
123
        'filename': os.path.basename(file_name),
H
Hsury 已提交
124
        'size': os.path.getsize(file_name),
H
Hsury 已提交
125
        'sha1': sha1,
W
wizardforcel 已提交
126
        'block': block_dicts,
H
Hsury 已提交
127
    }
H
Hsury 已提交
128
    meta = json.dumps(meta_dict, ensure_ascii=False).encode("utf-8")
W
wizardforcel 已提交
129
    full_meta = encoder.encode(meta)
W
wizardforcel 已提交
130 131 132 133 134 135 136 137
    r = api.image_upload(full_meta)
    if r['code'] == 0:
        url = r['data']['image_url']
        log("元数据上传完毕")
        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 -> {api.real2meta(url)}")
        write_history(first_4mb_sha1, meta_dict, url)
        return url
H
Hsury 已提交
138
    else:
W
wizardforcel 已提交
139 140 141 142 143 144 145 146
        log(f"元数据上传失败:{r.get('message')}")
        return


def tr_download(i, block_dict, f, offset):
    global succ

    url = block_dict['url']
W
wizardforcel 已提交
147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163
    for j in range(10):
        if not succ: break
        block = image_download(url)
        if not block:
            log(f"分块{i + 1}/{nblocks}{j + 1}次下载失败")
            if j == 9: succ = False
            continue
        block = encoder.decode(block)
        if calc_sha1(block) == block_dict['sha1']:
            with lock:
                f.seek(offset)
                f.write(block)
            log(f"分块{i + 1}/{nblocks}下载完毕")
            break
        else:
            log(f"分块{i + 1}/{nblocks}校验未通过")
            if j == 9: succ = False
W
wizardforcel 已提交
164
            
H
Hsury 已提交
165 166

def download_handle(args):
W
wizardforcel 已提交
167 168
    global succ
    global nblocks
H
Hsury 已提交
169

H
Hsury 已提交
170
    start_time = time.time()
H
Hsury 已提交
171
    meta_dict = fetch_meta(args.meta)
W
wizardforcel 已提交
172
    if not meta_dict:
H
Hsury 已提交
173
        log("元数据解析失败")
W
wizardforcel 已提交
174
        return
W
wizardforcel 已提交
175 176 177 178

    file_name = args.file if args.file else meta_dict['filename']
    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 已提交
179
    if os.path.exists(file_name):
W
wizardforcel 已提交
180
        if os.path.getsize(file_name) == meta_dict['size'] and calc_sha1(read_in_chunk(file_name)) == meta_dict['sha1']:
181
            log("文件已存在, 且与服务器端内容一致")
H
Hsury 已提交
182
            return file_name
W
wizardforcel 已提交
183
        if not args.force and not ask_overwrite():
W
wizardforcel 已提交
184
            return
W
wizardforcel 已提交
185 186 187 188 189 190 191 192 193 194 195 196 197 198 199

    log(f"线程数: {args.thread}")
    succ = True
    nblocks = len(meta_dict['block'])
    trpool = ThreadPoolExecutor(args.thread)
    hdls = []
    
    mode = "r+b" if os.path.exists(file_name) else "wb"
    with open(file_name, mode) as f:
        for i in range(nblocks):
            offset = block_offset(meta_dict, i)
            hdl = trpool.submit(tr_download, i, meta_dict['block'][i], f, offset)
            hdls.append(hdl)
        for h in hdls: h.result()
        if not succ: return
H
Hsury 已提交
200
        f.truncate(sum(block['size'] for block in meta_dict['block']))
W
wizardforcel 已提交
201
    
202
    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 已提交
203
    sha1 = calc_sha1(read_in_chunk(file_name))
H
Hsury 已提交
204
    if sha1 == meta_dict['sha1']:
205
        log("文件校验通过")
H
Hsury 已提交
206
        return file_name
H
Hsury 已提交
207
    else:
208
        log("文件校验未通过")
W
wizardforcel 已提交
209
        return
H
Hsury 已提交
210

H
Hsury 已提交
211 212 213
def info_handle(args):
    meta_dict = fetch_meta(args.meta)
    if meta_dict:
W
wizardforcel 已提交
214
        print_meta(meta_dict)
H
Hsury 已提交
215
    else:
W
wizardforcel 已提交
216
        log("元数据解析失败")
H
Hsury 已提交
217 218 219 220 221 222 223

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 已提交
224
            print(f"{' ' * len(prefix)} META URL -> {api.real2meta(meta_dict['url'])}")
H
Hsury 已提交
225 226 227 228
    else:
        print(f"暂无历史记录")

def main():
H
Hsury 已提交
229
    signal.signal(signal.SIGINT, lambda signum, frame: os.kill(os.getpid(), 9))
W
wizardforcel 已提交
230 231
    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 已提交
232
    subparsers = parser.add_subparsers()
W
wizardforcel 已提交
233
    
H
Hsury 已提交
234
    login_parser = subparsers.add_parser("login", help="log in to bilibili")
H
Hsury 已提交
235 236
    login_parser.add_argument("username", help="your bilibili username")
    login_parser.add_argument("password", help="your bilibili password")
H
Hsury 已提交
237
    login_parser.set_defaults(func=login_handle)
W
wizardforcel 已提交
238 239 240 241 242
    
    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 已提交
243 244 245
    userinfo_parser = subparsers.add_parser("userinfo", help="get userinfo")
    userinfo_parser.set_defaults(func=userinfo_handle)
    
H
Hsury 已提交
246
    upload_parser = subparsers.add_parser("upload", help="upload a file")
H
Hsury 已提交
247
    upload_parser.add_argument("file", help="name of the file to upload")
H
Hsury 已提交
248
    upload_parser.add_argument("-b", "--block-size", default=4, type=int, help="block size in MB")
H
Hsury 已提交
249
    upload_parser.add_argument("-t", "--thread", default=4, type=int, help="upload thread number")
H
Hsury 已提交
250
    upload_parser.set_defaults(func=upload_handle)
W
wizardforcel 已提交
251
    
H
Hsury 已提交
252
    download_parser = subparsers.add_parser("download", help="download a file")
H
Hsury 已提交
253
    download_parser.add_argument("meta", help="meta url")
H
Hsury 已提交
254
    download_parser.add_argument("file", nargs="?", default="", help="new file name")
H
Hsury 已提交
255
    download_parser.add_argument("-f", "--force", action="store_true", help="force to overwrite if file exists")
H
Hsury 已提交
256
    download_parser.add_argument("-t", "--thread", default=8, type=int, help="download thread number")
H
Hsury 已提交
257
    download_parser.set_defaults(func=download_handle)
W
wizardforcel 已提交
258
    
H
Hsury 已提交
259 260 261 262 263
    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 已提交
264 265 266
    shell = False
    while True:
        if shell:
W
wizardforcel 已提交
267
            args = shlex.split(input("BiliDriveEx > "))
H
Hsury 已提交
268 269 270 271 272
            try:
                args = parser.parse_args(args)
                args.func(args)
            except:
                pass
H
Hsury 已提交
273 274 275 276
        else:
            args = parser.parse_args()
            try:
                args.func(args)
H
Hsury 已提交
277
                break
W
wizardforcel 已提交
278 279
            except AttributeError as ex:
                traceback.print_exc(file=sys.stdout)
H
Hsury 已提交
280
                shell = True
H
Hsury 已提交
281 282 283 284 285 286 287
                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()