-
Notifications
You must be signed in to change notification settings - Fork 27
Expand file tree
/
Copy pathapi.py
More file actions
1414 lines (1261 loc) · 56.1 KB
/
api.py
File metadata and controls
1414 lines (1261 loc) · 56.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from functools import wraps
import re
import time
import json
import os
import logging
import pickle
import string
import random
import base64
from hashlib import sha1, md5
from urllib import urlencode, quote
from zlib import crc32
from requests_toolbelt import MultipartEncoder
import requests
requests.packages.urllib3.disable_warnings()
import rsa
import urllib
"""
logging.basicConfig(level=logging.DEBUG,
format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s',
datefmt='%a, %d %b %Y %H:%M:%S')
"""
BAIDUPAN_SERVER = 'pan.baidu.com'
BAIDUPCS_SERVER = 'pcs.baidu.com'
BAIDUPAN_HEADERS = {"Referer": "http://pan.baidu.com/disk/home",
"User-Agent": "netdisk;4.6.2.0;PC;PC-Windows;10.0.10240;WindowsBaiduYunGuanJia"}
# https://pcs.baidu.com/rest/2.0/pcs/manage?method=listhost -> baidu cdn
# uses CDN_DOMAIN/monitor.jpg to test speed for each CDN
api_template = 'http://%s/api/{0}' % BAIDUPAN_SERVER
class LoginFailed(Exception):
"""因为帐号原因引起的登录失败异常
如果是超时则是返回Timeout的异常
"""
pass
# experimental
class CancelledError(Exception):
"""
用户取消文件上传
"""
def __init__(self, msg):
self.msg = msg
Exception.__init__(self, msg)
def __str__(self):
return self.msg
__repr__ = __str__
class BufferReader(MultipartEncoder):
"""将multipart-formdata转化为stream形式的Proxy类
"""
def __init__(self, fields, boundary=None, callback=None, cb_args=(), cb_kwargs={}):
self._callback = callback
self._progress = 0
self._cb_args = cb_args
self._cb_kwargs = cb_kwargs
super(BufferReader, self).__init__(fields, boundary)
def read(self, size=None):
chunk = super(BufferReader, self).read(size)
self._progress += int(len(chunk))
self._cb_kwargs.update({
'size': self._len,
'progress': self._progress
})
if self._callback:
try:
self._callback(*self._cb_args, **self._cb_kwargs)
except: # catches exception from the callback
raise CancelledError('The upload was cancelled.')
return chunk
def check_login(func):
"""检查用户登录状态
这是pcs的检查方法
"""
@wraps(func)
def wrapper(*args, **kwargs):
ret = func(*args, **kwargs)
if type(ret) == requests.Response and re.search('application/json|text/html', ret.headers.get('content-type', '')):
try:
foo = json.loads(ret.content)
if foo.has_key('errno') and foo['errno'] == -6:
logging.debug(
'Offline, deleting cookies file then relogin.')
path = '.{0}.cookies'.format(args[0].username)
if os.path.exists(path):
os.remove(path)
args[0]._initiate()
except:
pass
return ret
return wrapper
class BaseClass(object):
"""提供PCS类的基本方法
"""
def __init__(self, username, password, api_template=api_template, captcha_func=None):
self.session = requests.session()
self.api_template = api_template
self.username = username
self.password = password
self.user = {}
self.progress_func = None
if captcha_func:
self.captcha_func = captcha_func
else:
self.captcha_func = self.show_captcha
# 设置pcs服务器
logging.debug('setting pcs server')
self.set_pcs_server(self.get_fastest_pcs_server())
self._initiate()
def get_fastest_pcs_server_test(self):
"""通过测试返回最快的pcs服务器
:returns: str -- 服务器地址
"""
ret = requests.get(
'https://pcs.baidu.com/rest/2.0/pcs/manage?method=listhost').content
serverlist = [server['host'] for server in json.loads(ret)['list']]
url_pattern = 'http://{0}/monitor.jpg'
time_record = []
for server in serverlist:
start = time.time() * 1000
requests.get(url_pattern.format(server))
end = time.time() * 1000
time_record.append((end - start, server))
logging.info('TEST %s %s ms' % (server, int(end - start)))
return min(time_record)[1]
def get_fastest_pcs_server(self):
"""通过百度返回设置最快的pcs服务器
"""
url = 'http://pcs.baidu.com/rest/2.0/pcs/file?app_id=250528&method=locateupload'
ret = requests.get(url).content
foo = json.loads(ret)
return foo['host']
def set_pcs_server(self, server):
"""手动设置百度pcs服务器
:params server: 服务器地址或域名
.. warning::
不要加 http:// 和末尾的 /
"""
global BAIDUPCS_SERVER
BAIDUPCS_SERVER = server
def _remove_empty_items(self, data):
for k, v in data.copy().items():
if v is None:
data.pop(k)
def _initiate(self):
if not self._load_cookies():
self.session.get('http://www.baidu.com', verify=False)
self.user['token'] = self._get_token()
self._login()
else:
self.user['token'] = self._get_token()
def _save_cookies(self):
cookies_file = '.{0}.cookies'.format(self.username)
with open(cookies_file, 'w') as f:
pickle.dump(
requests.utils.dict_from_cookiejar(self.session.cookies), f)
def _load_cookies(self):
cookies_file = '.{0}.cookies'.format(self.username)
logging.debug('cookies file:' + cookies_file)
if os.path.exists(cookies_file):
logging.debug('%s cookies file has already existed.' %
self.username)
with open(cookies_file) as cookies_file:
cookies = requests.utils.cookiejar_from_dict(
pickle.load(cookies_file))
logging.debug(str(cookies))
self.session.cookies = cookies
self.user['BDUSS'] = self.session.cookies['BDUSS']
return True
else:
return False
def _get_token(self):
# Token
ret = self.session.get(
'https://passport.baidu.com/v2/api/?getapi&tpl=mn&apiver=v3&class=login&tt=%s&logintype=dialogLogin&callback=0' % int(time.time()), verify=False).text.replace('\'', '\"')
foo = json.loads(ret)
logging.info('token %s' % foo['data']['token'])
return foo['data']['token']
def _get_captcha(self, code_string):
# Captcha
if code_string:
verify_code = self.captcha_func("https://passport.baidu.com/cgi-bin/genimage?" + code_string)
else:
verify_code = ""
return verify_code
def show_captcha(self, url_verify_code):
print(url_verify_code)
verify_code = raw_input('open url aboved with your web browser, then input verify code > ')
return verify_code
def _get_publickey(self):
url = 'https://passport.baidu.com/v2/getpublickey?token=' + \
self.user['token']
content = self.session.get(url, verify=False).content
jdata = json.loads(content.replace('\'','"'))
return (jdata['pubkey'], jdata['key'])
def _login(self):
# Login
#code_string, captcha = self._get_captcha()
captcha = ''
code_string = ''
pubkey, rsakey = self._get_publickey()
key = rsa.PublicKey.load_pkcs1_openssl_pem(pubkey)
password_rsaed = base64.b64encode(rsa.encrypt(self.password, key))
while True:
login_data = {'staticpage': 'http://www.baidu.com/cache/user/html/v3Jump.html',
'charset': 'UTF-8',
'token': self.user['token'],
'tpl': 'pp',
'subpro': '',
'apiver': 'v3',
'tt': str(int(time.time())),
'codestring': code_string,
'isPhone': 'false',
'safeflg': '0',
'u': 'https://passport.baidu.com/',
'quick_user': '0',
'logLoginType': 'pc_loginBasic',
'loginmerge': 'true',
'logintype': 'basicLogin',
'username': self.username,
'password': password_rsaed,
'verifycode': captcha,
'mem_pass': 'on',
'rsakey': str(rsakey),
'crypttype': 12,
'ppui_logintime': '50918',
'callback': 'parent.bd__pcbs__oa36qm'}
result = self.session.post(
'https://passport.baidu.com/v2/api/?login', data=login_data, verify=False)
# 是否需要验证码
if 'err_no=257' in result.content or 'err_no=6' in result.content:
code_string = re.findall('codeString=(.*?)&', result.content)[0]
logging.debug('need captcha, codeString=' + code_string)
captcha = self._get_captcha(code_string)
continue
break
# check exception
self._check_account_exception(result.content)
if not result.ok:
raise LoginFailed('Logging failed.')
logging.info('COOKIES' + str(self.session.cookies))
try:
self.user['BDUSS'] = self.session.cookies['BDUSS']
except:
raise LoginFailed('Logging failed.')
logging.info('user %s Logged in BDUSS: %s' %
(self.username, self.user['BDUSS']))
self._save_cookies()
def _check_account_exception(self, content):
err_id = re.findall('err_no=([\d]+)', content)[0]
if err_id == '0':
return
error_message = {
'-1':'系统错误, 请稍后重试',
'1':'您输入的帐号格式不正确',
'3':'验证码不存在或已过期,请重新输入',
'4': '您输入的帐号或密码有误',
'5': '请在弹出的窗口操作,或重新登录',
'6':'验证码输入错误',
'16': '您的帐号因安全问题已被限制登录',
'257': '需要验证码',
'100005': '系统错误, 请稍后重试',
'120016': '未知错误 120016',
'120019': '近期登录次数过多, 请先通过 passport.baidu.com 解除锁定',
'120021': '登录失败,请在弹出的窗口操作,或重新登录',
'500010': '登录过于频繁,请24小时后再试',
'400031': '账号异常,请在当前网络环境下在百度网页端正常登录一次',
'401007': '您的手机号关联了其他帐号,请选择登录'}
try:
msg = error_message[err_id]
except:
msg = 'unknown err_id=' + err_id
raise LoginFailed(msg)
def _params_utf8(self, params):
for k, v in params.items():
if isinstance(v, unicode):
params[k] = v.encode('utf-8')
@check_login
def _request(self, uri, method=None, url=None, extra_params=None,
data=None, files=None, callback=None, **kwargs):
params = {
'method': method,
'app_id': "250528",
'BDUSS': self.user['BDUSS'],
't': str(int(time.time())),
'bdstoken': self.user['token']
}
if extra_params:
params.update(extra_params)
self._remove_empty_items(params)
headers = dict(BAIDUPAN_HEADERS)
if 'headers' in kwargs:
headers.update(kwargs['headers'])
kwargs.pop('headers')
self._params_utf8(params)
if not url:
url = self.api_template.format(uri)
if data or files:
if '?' in url:
api = "%s&%s" % (url, urlencode(params))
else:
api = '%s?%s' % (url, urlencode(params))
# print params
if data:
self._remove_empty_items(data)
response = self.session.post(api, data=data, verify=False,
headers=headers, **kwargs)
else:
self._remove_empty_items(files)
body = BufferReader(files, callback=callback)
headers.update({
"Content-Type": body.content_type
}
)
response = self.session.post(
api, data=body, verify=False, headers=headers, **kwargs)
else:
api = url
if uri == 'filemanager' or uri == 'rapidupload' or uri == 'filemetas' or uri == 'precreate':
response = self.session.post(
api, params=params, verify=False, headers=headers, **kwargs)
else:
response = self.session.get(
api, params=params, verify=False, headers=headers, **kwargs)
return response
class PCS(BaseClass):
def __init__(self, username, password, captcha_callback=None):
"""
:param username: 百度网盘的用户名
:type username: str
:param password: 百度网盘的密码
:type password: str
:param captcha_callback: 验证码的回调函数
.. note::
该函数会获得一个jpeg文件的内容,返回值需为验证码
"""
super(PCS, self).__init__(username, password, api_template, captcha_func=captcha_callback)
def __err_handler(self, act, errno, callback=None, args=(), kwargs={}):
"""百度网盘下载错误控制
:param act: 出错时的行为, 有 download
:param errno: 出错时的errno,这个要配合act才有实际意义
:param callback: 返回时的调用函数, 为空时返回None
:param args: 给callback函数的参数tuple
:param kwargs: 给callback函数的带名参数字典
在本函数调用后一定可以解决提交过来的问题, 在外部不需要重复检查是否存在原问题
"""
errno = int(errno)
def err_handler_download():
if errno == 112:
# 页面失效, 重新刷新页面
url = 'http://pan.baidu.com/disk/home'
self.session.get(url, verify=False)
return
def err_handler_upload():
# 实际出问题了再写
return
def err_handler_generic():
return
_act = {'download': err_handler_download,
'upload': err_handler_upload,
'generic': err_handler_generic
}
if act not in _act:
raise Exception('行为未定义, 无法处理该行为的错误')
if callback:
return callback(*args, **kwargs)
return None
def quota(self, **kwargs):
"""获得配额信息
:return requests.Response
.. note::
返回正确时返回的 Reponse 对象 content 中的数据结构
{"errno":0,"total":配额字节数,"used":已使用字节数,"request_id":请求识别号}
"""
return self._request('quota', **kwargs)
def upload(self, dir, file_handler, filename, ondup="newcopy", callback=None, **kwargs):
"""上传单个文件(<2G).
| 百度PCS服务目前支持最大2G的单个文件上传。
| 如需支持超大文件(>2G)的断点续传,请参考下面的“分片文件上传”方法。
:param dir: 网盘中文件的保存路径(不包含文件名)。
必须以 / 开头。
.. warning::
* 注意本接口的 dir 参数不包含文件名,只包含路径
* 路径长度限制为1000;
* 径中不能包含以下字符:``\\\\ ? | " > < : *``;
* 文件名或路径名开头结尾不能是 ``.``
或空白字符,空白字符包括:
``\\r, \\n, \\t, 空格, \\0, \\x0B`` 。
:param file_handler: 上传文件对象 。(e.g. ``open('foobar', 'rb')`` )
.. warning::
注意不要使用 .read() 方法.
:type file_handler: file
:param callback: 上传进度回调函数
需要包含 size 和 progress 名字的参数
:param filename:
:param ondup: (可选)
* 'overwrite':表示覆盖同名文件;
* 'newcopy':表示生成文件副本并进行重命名,命名规则为“
文件名_日期.后缀”。
:return: requests.Response 对象
.. note::
返回正确时返回的 Reponse 对象 content 中的数据结构
{"path":"服务器文件路径","size":文件大小,"ctime":创建时间,"mtime":修改时间,"md5":"文件md5值","fs_id":服务器文件识别号,"isdir":是否为目录,"request_id":请求识别号}
"""
params = {
'dir': dir,
'ondup': ondup,
'filename': filename
}
tmp_filename = ''.join(random.sample(string.ascii_letters, 10))
files = {'file': (tmp_filename, file_handler)}
url = 'https://{0}/rest/2.0/pcs/file'.format(BAIDUPCS_SERVER)
return self._request('file', 'upload', url=url, extra_params=params,
files=files, callback=callback, **kwargs)
def upload_tmpfile(self, file_handler, callback=None, **kwargs):
"""分片上传—文件分片及上传.
百度 PCS 服务支持每次直接上传最大2G的单个文件。
如需支持上传超大文件(>2G),则可以通过组合调用分片文件上传的
``upload_tmpfile`` 方法和 ``upload_superfile`` 方法实现:
1. 首先,将超大文件分割为2G以内的单文件,并调用 ``upload_tmpfile``
将分片文件依次上传;
2. 其次,调用 ``upload_superfile`` ,完成分片文件的重组。
除此之外,如果应用中需要支持断点续传的功能,
也可以通过分片上传文件并调用 ``upload_superfile`` 接口的方式实现。
:param file_handler: 上传文件对象 。(e.g. ``open('foobar', 'rb')`` )
.. warning::
注意不要使用 .read() 方法.
:type file_handler: file
:param callback: 上传进度回调函数
需要包含 size 和 progress 名字的参数
:param ondup: (可选)
* 'overwrite':表示覆盖同名文件;
* 'newcopy':表示生成文件副本并进行重命名,命名规则为“
文件名_日期.后缀”。
:type ondup: str
:return: requests.Response
.. note::
这个对象的内容中的 md5 字段为合并文件的凭依
.. note::
返回正确时返回的 Reponse 对象 content 中的数据结构
{"md5":"片段的 md5 值","request_id":请求识别号}
"""
params = {
'type': 'tmpfile'
}
files = {'file': (str(int(time.time())), file_handler)}
url = 'https://{0}/rest/2.0/pcs/file'.format(BAIDUPCS_SERVER)
return self._request('file', 'upload', url=url, extra_params=params, callback=callback,
files=files, **kwargs)
def upload_superfile(self, remote_path, block_list, ondup="newcopy", **kwargs):
"""分片上传—合并分片文件.
与分片文件上传的 ``upload_tmpfile`` 方法配合使用,
可实现超大文件(>2G)上传,同时也可用于断点续传的场景。
:param remote_path: 网盘中文件的保存路径(包含文件名)。
必须以 开头。
.. warning::
* 路径长度限制为1000;
* 径中不能包含以下字符:``\\\\ ? | " > < : *``;
* 文件名或路径名开头结尾不能是 ``.``
或空白字符,空白字符包括:
``\\r, \\n, \\t, 空格, \\0, \\x0B`` 。
:param block_list: 子文件内容的 MD5 值列表;子文件至少两个,最多1024个。
:type block_list: list
:param ondup: (可选)
* 'overwrite':表示覆盖同名文件;
* 'newcopy':表示生成文件副本并进行重命名,命名规则为“
文件名_日期.后缀”。
:return: Response 对象
.. note::
返回正确时返回的 Reponse 对象 content 中的数据结构
{"path":"服务器文件路径","size":文件大小,"ctime":创建时间,"mtime":修改时间,"md5":"文件md5值","fs_id":服务器文件识别号,"isdir":是否为目录,"request_id":请求识别号}
"""
params = {
'path': remote_path,
'ondup': ondup
}
data = {
'param': json.dumps({'block_list': block_list}),
}
url = 'https://{0}/rest/2.0/pcs/file'.format(BAIDUPCS_SERVER)
return self._request('file', 'createsuperfile', url=url, extra_params=params,
data=data, **kwargs)
def get_sign(self):
# refered:
# https://github.com/PeterDing/iScript/blob/master/pan.baidu.com.py
url = 'http://pan.baidu.com/disk/home'
r = self.session.get(url, verify=False)
html = r.content
sign1 = re.search(r'"sign1":"([A-Za-z0-9]+)"', html).group(1)
sign3 = re.search(r'"sign3":"([A-Za-z0-9]+)"', html).group(1)
timestamp = re.search(r'"timestamp":([0-9]+)[^0-9]', html).group(1)
def sign2(j, r):
a = []
p = []
o = ''
v = len(j)
for q in xrange(256):
a.append(ord(j[q % v]))
p.append(q)
u = 0
for q in xrange(256):
u = (u + p[q] + a[q]) % 256
t = p[q]
p[q] = p[u]
p[u] = t
i = 0
u = 0
for q in xrange(len(r)):
i = (i + 1) % 256
u = (u + p[i]) % 256
t = p[i]
p[i] = p[u]
p[u] = t
k = p[((p[i] + p[u]) % 256)]
o += chr(ord(r[q]) ^ k)
return base64.b64encode(o)
self.dsign = sign2(sign3, sign1)
self.timestamp = timestamp
def _locatedownload(self, remote_path, **kwargs):
"""百度云管家获得方式
:param remote_path: 需要下载的文件路径
:type remote_path: str
"""
params = {
'path': remote_path
}
url = 'https://{0}/rest/2.0/pcs/file'.format(BAIDUPCS_SERVER)
return self._request('file', 'locatedownload', url=url,
extra_params=params, **kwargs)
def _yunguanjia_format(self, remote_path, **kwargs):
ret = self._locatedownload(remote_path, **kwargs).content
data = json.loads(ret)
return 'http://' + data['host'] + data['path']
def download_url(self, remote_path, **kwargs):
"""返回目标文件可用的下载地址
:param remote_path: 每一项代表需要下载的文件路径
:type remote_path: str list
"""
def get_url(dlink):
return self.session.get(dlink,
headers=BAIDUPAN_HEADERS,
stream=True, verify=False).url
if not hasattr(self, 'dsign'):
self.get_sign()
if isinstance(remote_path, str) or isinstance(remote_path, unicode):
remote_path = [remote_path]
file_list = []
jdata = json.loads(self.meta(remote_path).content)
if jdata['errno'] != 0:
jdata = self.__err_handler('generic', jdata['errno'],
self.meta,
args=(remote_path,)
)
logging.debug('[*]' + str(jdata))
for i, entry in enumerate(jdata['info']):
url = entry['dlink']
foo = get_url(url)
if 'wenxintishi' in foo:
file_list.append(self._yunguanjia_format(remote_path[i]))
else:
file_list.append(get_url(entry['dlink']))
return file_list
# Deprecated
# using download_url to get real download url
def download(self, remote_path, **kwargs):
"""下载单个文件。
download 接口支持HTTP协议标准range定义,通过指定range的取值可以实现
断点下载功能。 例如:如果在request消息中指定“Range: bytes=0-99”,
那么响应消息中会返回该文件的前100个字节的内容;
继续指定“Range: bytes=100-199”,
那么响应消息中会返回该文件的第二个100字节内容::
>>> headers = {'Range': 'bytes=0-99'}
>>> pcs = PCS('username','password')
>>> pcs.download('/test_sdk/test.txt', headers=headers)
:param remote_path: 网盘中文件的路径(包含文件名)。
必须以 / 开头。
.. warning::
* 路径长度限制为1000;
* 径中不能包含以下字符:``\\\\ ? | " > < : *``;
* 文件名或路径名开头结尾不能是 ``.``
或空白字符,空白字符包括:
``\\r, \\n, \\t, 空格, \\0, \\x0B`` 。
:return: requests.Response 对象
"""
params = {
'path': remote_path,
}
url = 'https://{0}/rest/2.0/pcs/file'.format(BAIDUPCS_SERVER)
return self._request('file', 'download', url=url,
extra_params=params, **kwargs)
def get_streaming(self, path, stype="M3U8_AUTO_480", **kwargs):
"""获得视频的m3u8列表
:param path: 视频文件路径
:param type: 返回stream类型, 已知有``M3U8_AUTO_240``/``M3U8_AUTO_480``/``M3U8_AUTO_720``
.. warning::
M3U8_AUTO_240会有问题, 目前480P是最稳定的, 也是百度网盘默认的
:return: str 播放(列表)需要的信息
"""
params = {
'path': path,
'type': stype
}
url = 'https://{0}/rest/2.0/pcs/file'.format(BAIDUPCS_SERVER)
while True:
ret = self._request('file', 'streaming', url=url, extra_params=params, **kwargs)
if not ret.ok:
logging.debug('get_streaming ret_status_code %s' % ret.status_code)
jdata = json.loads(ret.content)
if jdata['error_code'] == 31345:
# 再试一次
continue
elif jdata['error_code'] == 31066:
# 文件不存在
return 31066
elif jdata['error_code'] == 31304:
# 文件类型不支持
return 31304
elif jdata['error_code'] == 31023:
# params error
return 31023
return ret.content
def mkdir(self, remote_path, **kwargs):
"""为当前用户创建一个目录.
:param remote_path: 网盘中目录的路径,必须以 / 开头。
.. warning::
* 路径长度限制为1000;
* 径中不能包含以下字符:``\\\\ ? | " > < : *``;
* 文件名或路径名开头结尾不能是 ``.``
或空白字符,空白字符包括:
``\\r, \\n, \\t, 空格, \\0, \\x0B`` 。
:return: Response 对象
.. note::
返回正确时返回的 Reponse 对象 content 中的数据结构
{"fs_id":服务器文件识别号,"path":"路径","ctime":创建时间,"mtime":修改时间,"status":0,"isdir":1,"errno":0,"name":"文件路径"}
"""
data = {
'path': remote_path,
'isdir': "1",
"size": "",
"block_list": "[]"
}
# 奇怪的是创建新目录的method是post
return self._request('create', 'post', data=data, **kwargs)
def list_files(self, remote_path, by="name", order="desc",
limit=None, **kwargs):
"""获取目录下的文件列表.
:param remote_path: 网盘中目录的路径,必须以 / 开头。
.. warning::
* 路径长度限制为1000;
* 径中不能包含以下字符:``\\\\ ? | " > < : *``;
* 文件名或路径名开头结尾不能是 ``.``
或空白字符,空白字符包括:
``\\r, \\n, \\t, 空格, \\0, \\x0B`` 。
:param by: 排序字段,缺省根据文件类型排序:
* time(修改时间)
* name(文件名)
* size(大小,注意目录无大小)
:param order: “asc”或“desc”,缺省采用降序排序。
* asc(升序)
* desc(降序)
:param limit: 返回条目控制,参数格式为:n1-n2。
返回结果集的[n1, n2)之间的条目,缺省返回所有条目;
n1从0开始。
:return: requests.Response 对象
.. note::
返回正确时返回的 Reponse 对象 content 中的数据结构
{
"errno":0,
"list":[
{"fs_id":服务器文件识别号"path":"路径","server_filename":"服务器文件名(不汗含路径)","size":文件大小,"server_mtime":服务器修改时间,"server_ctime":服务器创建时间,"local_mtime":本地修改时间,"local_ctime":本地创建时间,"isdir":是否是目录,"category":类型,"md5":"md5值"}……等等
],
"request_id":请求识别号
}
"""
if order == "desc":
desc = "1"
else:
desc = "0"
params = {
'dir': remote_path,
'order': by,
'desc': desc
}
return self._request('list', 'list', extra_params=params, **kwargs)
def move(self, path_list, dest, **kwargs):
"""
移动文件或文件夹
:param path_list: 在百度盘上要移动的源文件path
:type path_list: list
:param dest: 要移动到的目录
:type dest: str
"""
def __path(path):
if path.endswith('/'):
return path.split('/')[-2]
else:
return os.path.basename(path)
params = {
'opera': 'move'
}
data = {
'filelist': json.dumps([{
"path": path,
"dest": dest,
"newname": __path(path)} for path in path_list]),
}
url = 'http://{0}/api/filemanager'.format(BAIDUPAN_SERVER)
return self._request('filemanager', 'move', url=url, data=data, extra_params=params, **kwargs)
def rename(self, rename_pair_list, **kwargs):
"""重命名
:param rename_pair_list: 需要重命名的文件(夹)pair (路径,新名称)列表,如[('/aa.txt','bb.txt')]
:type rename_pair_list: list
"""
foo = []
for path, newname in rename_pair_list:
foo.append({'path': path,
'newname': newname
})
data = {'filelist': json.dumps(foo)}
params = {
'opera': 'rename'
}
url = 'http://{0}/api/filemanager'.format(BAIDUPAN_SERVER)
print '请求url', url
logging.debug('rename ' + str(data) + 'URL:' + url)
return self._request('filemanager', 'rename', url=url, data=data, extra_params=params, **kwargs)
def copy(self, path_list, dest, **kwargs):
"""
复制文件或文件夹
:param path_list: 在百度盘上要复制的源文件path
:type path_list: list
:param dest: 要复制到的目录
:type dest: str
"""
def __path(path):
if path.endswith('/'):
return path.split('/')[-2]
else:
return os.path.basename(path)
params = {
'opera': 'copy'
}
data = {
'filelist': json.dumps([{
"path": path,
"dest": dest,
"newname": __path(path)} for path in path_list]),
}
url = 'http://{0}/api/filemanager'.format(BAIDUPAN_SERVER)
return self._request('filemanager', 'move', url=url, data=data, extra_params=params, **kwargs)
def delete(self, path_list, **kwargs):
"""
删除文件或文件夹
:param path_list: 待删除的文件或文件夹列表,每一项为服务器路径
:type path_list: list
"""
data = {
'filelist': json.dumps([path for path in path_list])
}
url = 'http://{0}/api/filemanager?opera=delete'.format(BAIDUPAN_SERVER)
return self._request('filemanager', 'delete', url=url, data=data, **kwargs)
def share(self, file_ids, pwd=None, **kwargs):
"""
创建一个文件的分享链接
:param file_ids: 要分享的文件fid列表
:type path_list: list
:param pwd: 分享密码,没有则没有密码
:type pwd: str
:return: requests.Response 对象
.. note::
返回正确
{
"errno": 0,
"request_id": 请求识别号,
"shareid": 分享识别号,
"link": "分享地址",
"shorturl": "段网址",
"ctime": 创建时间,
"premis": false
}
"""
if pwd:
data = {
'fid_list': json.dumps([int(fid) for fid in file_ids]),
'pwd': pwd,
'schannel': 4,
'channel_list': json.dumps([])
}
else:
data = {
'fid_list': json.dumps([int(fid) for fid in file_ids]),
'schannel': 0,
'channel_list': json.dumps([])
}
url = 'http://pan.baidu.com/share/set'
return self._request('share/set', '', url=url, data=data, **kwargs)
def list_streams(self, file_type, start=0, limit=1000, order='time', desc='1',
filter_path=None, **kwargs):
"""以视频、音频、图片及文档四种类型的视图获取所创建应用程序下的
文件列表.
:param file_type: 类型分为video audio image doc other exe torrent
:param start: 返回条目控制起始值,缺省值为0。
:param limit: 返回条目控制长度,缺省为1000,可配置。
:param filter_path: 需要过滤的前缀路径,如:/album
.. warning::
* 路径长度限制为1000;
* 径中不能包含以下字符:``\\\\ ? | " > < : *``;
* 文件名或路径名开头结尾不能是 ``.``
或空白字符,空白字符包括:
``\\r, \\n, \\t, 空格, \\0, \\x0B`` 。
:return: requests.Response 对象, 结构和 list_files 相同
"""
if file_type == 'doc':
file_type = '4'
elif file_type == 'video':
file_type = '1'
elif file_type == 'image':
file_type = '3'
elif file_type == 'torrent':
file_type = '7'
elif file_type == 'other':
file_type = '6'
elif file_type == 'audio':
file_type = '2'
elif file_type == 'exe':
file_type = '5'
params = {
'category': file_type,
'pri': '-1',
'start': start,
'num': limit,
'order': order,
'desc': desc,
'filter_path': filter_path,
}
url = 'http://pan.baidu.com/api/categorylist'
return self._request('categorylist', 'list', url=url, extra_params=params,
**kwargs)
def add_download_task(self, source_url, remote_path, selected_idx=(), **kwargs):
"""
添加离线任务,支持所有百度网盘支持的类型
"""
if source_url.startswith('magnet:?'):
print('Magnet: "%s"' % source_url)
return self.add_magnet_task(source_url, remote_path, selected_idx, **kwargs)
elif source_url.endswith('.torrent'):
print('BitTorrent: "%s"' % source_url)
return self.add_torrent_task(source_url, remote_path, selected_idx, **kwargs)
else:
print('Others: "%s"' % source_url)
data = {
'method': 'add_task',
'source_url': source_url,
'save_path': remote_path,
}
url = 'http://{0}/rest/2.0/services/cloud_dl'.format(BAIDUPAN_SERVER)
return self._request('services/cloud_dl', 'add_task', url=url,
data=data, **kwargs)
def add_torrent_task(self, torrent_path, save_path='/', selected_idx=(), **kwargs):
"""
添加本地BT任务
:param torrent_path: 本地种子的路径
:param save_path: 远程保存路径
:param selected_idx: 要下载的文件序号 —— 集合为空下载所有,非空集合指定序号集合,空串下载默认
:return: requests.Response
.. note::
返回正确时返回的 Reponse 对象 content 中的数据结构
{"task_id":任务编号,"rapid_download":是否已经完成(急速下载),"request_id":请求识别号}
"""
# 上传种子文件
torrent_handler = open(torrent_path, 'rb')
basename = os.path.basename(torrent_path)
# 清理同名文件
self.delete(['/' + basename])
response = self.upload('/', torrent_handler, basename).json()
remote_path = response['path']
logging.debug('REMOTE PATH:' + remote_path)
# 获取种子信息
response = self._get_torrent_info(remote_path).json()
if response.get('error_code'):
print(response.get('error_code'))
return
if not response['torrent_info']['file_info']:
return
# 要下载的文件序号:集合为空下载所有,非空集合指定序号集合,空串下载默认