p115client 0.0.5.8.2__py3-none-any.whl → 0.0.5.8.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
p115client/client.py CHANGED
@@ -9,15 +9,14 @@ __all__ = [
9
9
  "normalize_attr_app", "normalize_attr_app2", "P115OpenClient", "P115Client",
10
10
  ]
11
11
 
12
- import errno
13
-
14
- from asyncio import create_task, get_running_loop, run_coroutine_threadsafe, to_thread, Lock as AsyncLock
12
+ from asyncio import Lock as AsyncLock
15
13
  from base64 import b64encode
16
14
  from collections.abc import (
17
15
  AsyncGenerator, AsyncIterable, Awaitable, Buffer, Callable, Coroutine, Generator,
18
16
  ItemsView, Iterable, Iterator, Mapping, MutableMapping, Sequence,
19
17
  )
20
18
  from datetime import date, datetime, timedelta
19
+ from errno import EBUSY, EEXIST, EFBIG, EINVAL, EIO, EISDIR, ENODATA, ENOENT, ENOSPC, ENOSYS, ENOTSUP
21
20
  from functools import partial
22
21
  from hashlib import md5, sha1
23
22
  from http.cookiejar import Cookie, CookieJar
@@ -230,7 +229,7 @@ def json_loads(content: Buffer, /):
230
229
  except Exception as e:
231
230
  if isinstance(content, memoryview):
232
231
  content = content.tobytes()
233
- raise DataError(errno.ENODATA, content) from e
232
+ raise DataError(ENODATA, content) from e
234
233
 
235
234
 
236
235
  def default_parse(resp, content: Buffer, /):
@@ -283,35 +282,6 @@ def items(m: Mapping, /) -> ItemsView:
283
282
  return ItemsView(m)
284
283
 
285
284
 
286
- def file_close(file, /, async_: bool = False):
287
- cls = type(file)
288
- if async_:
289
- aclose = getattr(file, "aclose", None)
290
- if callable(aclose):
291
- return aclose()
292
- aeixt = getattr(cls, "__aexit__", None)
293
- if callable(aeixt):
294
- return aeixt(file, *exc_info())
295
- close = getattr(file, "close", None)
296
- if callable(close):
297
- if async_:
298
- return ensure_async(close, threaded=True)()
299
- else:
300
- return close()
301
- exit = getattr(cls, "__exit__", None)
302
- if callable(exit):
303
- if async_:
304
- return ensure_async(exit, threaded=True)(file, *exc_info())
305
- else:
306
- return exit(file, *exc_info())
307
- deleter = getattr(cls, "__del__", None)
308
- if callable(deleter):
309
- if async_:
310
- return ensure_async(deleter, threaded=True)(file)
311
- else:
312
- return deleter(file)
313
-
314
-
315
285
  def cookies_equal(cookies1: None | str, cookies2: None | str, /) -> bool:
316
286
  if not (cookies1 and cookies2):
317
287
  return False
@@ -380,7 +350,7 @@ def check_response(resp: dict | Awaitable[dict], /) -> dict | Coroutine[Any, Any
380
350
  """
381
351
  def check(resp, /) -> dict:
382
352
  if not isinstance(resp, dict):
383
- raise P115OSError(errno.EIO, resp)
353
+ raise P115OSError(EIO, resp)
384
354
  if resp.get("state", True):
385
355
  return resp
386
356
  if code := get_first(resp, "errno", "errNo", "errcode", "errCode", "code"):
@@ -390,108 +360,109 @@ def check_response(resp: dict | Awaitable[dict], /) -> dict | Coroutine[Any, Any
390
360
  match code:
391
361
  # {"state": false, "errno": 99, "error": "请重新登录"}
392
362
  case 99:
393
- raise LoginError(errno.EIO, resp)
363
+ raise LoginError(EIO, resp)
394
364
  # {"state": false, "errno": 911, "error": "请验证账号"}
395
365
  case 911:
396
- raise AuthenticationError(errno.EIO, resp)
366
+ raise AuthenticationError(EIO, resp)
397
367
  # {"state": false, "errno": 20001, "error": "目录名称不能为空"}
398
368
  case 20001:
399
- raise OperationalError(errno.EINVAL, resp)
369
+ raise OperationalError(EINVAL, resp)
400
370
  # {"state": false, "errno": 20004, "error": "该目录名称已存在。"}
401
371
  case 20004:
402
- raise FileExistsError(errno.EEXIST, resp)
372
+ raise FileExistsError(EEXIST, resp)
403
373
  # {"state": false, "errno": 20009, "error": "父目录不存在。"}
404
374
  case 20009:
405
- raise FileNotFoundError(errno.ENOENT, resp)
375
+ raise FileNotFoundError(ENOENT, resp)
406
376
  # {"state": false, "errno": 20018, "error": "文件不存在或已删除。"}
407
377
  # {"state": false, "errno": 50015, "error": "文件不存在或已删除。"}
408
- case 20018 | 50015:
409
- raise FileNotFoundError(errno.ENOENT, resp)
378
+ # {"state": false, "errno": 430004, "error": "文件(夹)不存在或已删除。"}
379
+ case 20018 | 50015 | 430004:
380
+ raise FileNotFoundError(ENOENT, resp)
410
381
  # {"state": false, "errno": 20020, "error": "后缀名不正确,请重新输入"}
411
382
  case 20020:
412
- raise OperationalError(errno.ENOTSUP, resp)
383
+ raise OperationalError(ENOTSUP, resp)
413
384
  # {"state": false, "errno": 20021, "error": "后缀名不正确,请重新输入"}
414
385
  case 20021:
415
- raise OperationalError(errno.ENOTSUP, resp)
386
+ raise OperationalError(ENOTSUP, resp)
416
387
  # {"state": false, "errno": 31001, "error": "所预览的文件不存在。"}
417
388
  case 31001:
418
- raise FileNotFoundError(errno.ENOENT, resp)
389
+ raise FileNotFoundError(ENOENT, resp)
419
390
  # {"state": false, "errno": 31004, "error": "文档未上传完整,请上传完成后再进行查看。"}
420
391
  case 31004:
421
- raise FileNotFoundError(errno.ENOENT, resp)
392
+ raise FileNotFoundError(ENOENT, resp)
422
393
  # {"state": false, "errno": 50003, "error": "很抱歉,该文件提取码不存在。"}
423
394
  case 50003:
424
- raise FileNotFoundError(errno.ENOENT, resp)
395
+ raise FileNotFoundError(ENOENT, resp)
425
396
  # {"state": false, "errno": 90008, "error": "文件(夹)不存在或已经删除。"}
426
397
  case 90008:
427
- raise FileNotFoundError(errno.ENOENT, resp)
398
+ raise FileNotFoundError(ENOENT, resp)
428
399
  # {"state": false, "errno": 91002, "error": "不能将文件复制到自身或其子目录下。"}
429
400
  case 91002:
430
- raise NotSupportedError(errno.ENOTSUP, resp)
401
+ raise NotSupportedError(ENOTSUP, resp)
431
402
  # {"state": false, "errno": 91004, "error": "操作的文件(夹)数量超过5万个"}
432
403
  case 91004:
433
- raise NotSupportedError(errno.ENOTSUP, resp)
404
+ raise NotSupportedError(ENOTSUP, resp)
434
405
  # {"state": false, "errno": 91005, "error": "空间不足,复制失败。"}
435
406
  case 91005:
436
- raise OperationalError(errno.ENOSPC, resp)
407
+ raise OperationalError(ENOSPC, resp)
437
408
  # {"state": false, "errno": 231011, "error": "文件已删除,请勿重复操作"}
438
409
  case 231011:
439
- raise FileNotFoundError(errno.ENOENT, resp)
410
+ raise FileNotFoundError(ENOENT, resp)
440
411
  # {"state": false, "errno": 300104, "error": "文件超过200MB,暂不支持播放"}
441
412
  case 300104:
442
- raise P115OSError(errno.EFBIG, resp)
413
+ raise P115OSError(EFBIG, resp)
443
414
  # {"state": false, "errno": 590075, "error": "操作太频繁,请稍候再试"}
444
415
  case 590075:
445
- raise BusyOSError(errno.EBUSY, resp)
416
+ raise BusyOSError(EBUSY, resp)
446
417
  # {"state": false, "errno": 800001, "error": "目录不存在。"}
447
418
  case 800001:
448
- raise FileNotFoundError(errno.ENOENT, resp)
419
+ raise FileNotFoundError(ENOENT, resp)
449
420
  # {"state": false, "errno": 980006, "error": "404 Not Found"}
450
421
  case 980006:
451
- raise NotSupportedError(errno.ENOSYS, resp)
422
+ raise NotSupportedError(ENOSYS, resp)
452
423
  # {"state": false, "errno": 990001, "error": "登陆超时,请重新登陆。"}
453
424
  case 990001:
454
425
  # NOTE: 可能就是被下线了
455
- raise AuthenticationError(errno.EIO, resp)
426
+ raise AuthenticationError(EIO, resp)
456
427
  # {"state": false, "errno": 990002, "error": "参数错误。"}
457
428
  case 990002:
458
- raise P115OSError(errno.EINVAL, resp)
429
+ raise P115OSError(EINVAL, resp)
459
430
  # {"state": false, "errno": 990003, "error": "操作失败。"}
460
431
  case 990003:
461
- raise OperationalError(errno.EIO, resp)
432
+ raise OperationalError(EIO, resp)
462
433
  # {"state": false, "errno": 990005, "error": "你的账号有类似任务正在处理,请稍后再试!"}
463
434
  case 990005:
464
- raise BusyOSError(errno.EBUSY, resp)
435
+ raise BusyOSError(EBUSY, resp)
465
436
  # {"state": false, "errno": 990009, "error": "删除[...]操作尚未执行完成,请稍后再试!"}
466
437
  # {"state": false, "errno": 990009, "error": "还原[...]操作尚未执行完成,请稍后再试!"}
467
438
  # {"state": false, "errno": 990009, "error": "复制[...]操作尚未执行完成,请稍后再试!"}
468
439
  # {"state": false, "errno": 990009, "error": "移动[...]操作尚未执行完成,请稍后再试!"}
469
440
  case 990009:
470
- raise BusyOSError(errno.EBUSY, resp)
441
+ raise BusyOSError(EBUSY, resp)
471
442
  # {"state": false, "errno": 990023, "error": "操作的文件(夹)数量超过5万个"}
472
443
  case 990023:
473
- raise OperationalError(errno.ENOTSUP, resp)
444
+ raise OperationalError(ENOTSUP, resp)
474
445
  # {"state": 0, "errno": 40100000, "error": "参数错误!"}
475
446
  case 40100000:
476
- raise OperationalError(errno.EINVAL, resp)
447
+ raise OperationalError(EINVAL, resp)
477
448
  # {"state": 0, "errno": 40101004, "error": "IP登录异常,请稍候再登录!"}
478
449
  case 40101004:
479
- raise LoginError(errno.EIO, resp)
450
+ raise LoginError(EIO, resp)
480
451
  # {"state": 0, "errno": 40101017, "error": "用户验证失败!"}
481
452
  case 40101017:
482
- raise AuthenticationError(errno.EIO, resp)
453
+ raise AuthenticationError(EIO, resp)
483
454
  # {"state": 0, "errno": 40101032, "error": "请重新登录"}
484
455
  case 40101032:
485
- raise LoginError(errno.EIO, resp)
456
+ raise LoginError(EIO, resp)
486
457
  elif "msg_code" in resp:
487
458
  match resp["msg_code"]:
488
459
  case 50028:
489
- raise P115OSError(errno.EFBIG, resp)
460
+ raise P115OSError(EFBIG, resp)
490
461
  case 70004:
491
- raise IsADirectoryError(errno.EISDIR, resp)
462
+ raise IsADirectoryError(EISDIR, resp)
492
463
  case 70005 | 70008:
493
- raise FileNotFoundError(errno.ENOENT, resp)
494
- raise P115OSError(errno.EIO, resp)
464
+ raise FileNotFoundError(ENOENT, resp)
465
+ raise P115OSError(EIO, resp)
495
466
  if isinstance(resp, dict):
496
467
  return check(resp)
497
468
  elif isawaitable(resp):
@@ -499,7 +470,7 @@ def check_response(resp: dict | Awaitable[dict], /) -> dict | Coroutine[Any, Any
499
470
  return check(await resp)
500
471
  return check_await()
501
472
  else:
502
- raise P115OSError(errno.EIO, resp)
473
+ raise P115OSError(EIO, resp)
503
474
 
504
475
 
505
476
  def normalize_attr_web(
@@ -1773,11 +1744,11 @@ class ClientRequestMixin:
1773
1744
  print("[status=2] qrcode: signed in")
1774
1745
  break
1775
1746
  case -1:
1776
- raise LoginError(errno.EIO, "[status=-1] qrcode: expired")
1747
+ raise LoginError(EIO, "[status=-1] qrcode: expired")
1777
1748
  case -2:
1778
- raise LoginError(errno.EIO, "[status=-2] qrcode: canceled")
1749
+ raise LoginError(EIO, "[status=-2] qrcode: canceled")
1779
1750
  case _:
1780
- raise LoginError(errno.EIO, f"qrcode: aborted with {resp!r}")
1751
+ raise LoginError(EIO, f"qrcode: aborted with {resp!r}")
1781
1752
  if app:
1782
1753
  return cls.login_qrcode_scan_result(
1783
1754
  login_uid,
@@ -1871,11 +1842,11 @@ class ClientRequestMixin:
1871
1842
  print("[status=2] qrcode: signed in")
1872
1843
  break
1873
1844
  case -1:
1874
- raise LoginError(errno.EIO, "[status=-1] qrcode: expired")
1845
+ raise LoginError(EIO, "[status=-1] qrcode: expired")
1875
1846
  case -2:
1876
- raise LoginError(errno.EIO, "[status=-2] qrcode: canceled")
1847
+ raise LoginError(EIO, "[status=-2] qrcode: canceled")
1877
1848
  case _:
1878
- raise LoginError(errno.EIO, f"qrcode: aborted with {resp!r}")
1849
+ raise LoginError(EIO, f"qrcode: aborted with {resp!r}")
1879
1850
  return cls.login_qrcode_access_token_open(
1880
1851
  login_uid,
1881
1852
  async_=async_,
@@ -2599,7 +2570,7 @@ class P115OpenClient(ClientRequestMixin):
2599
2570
  url = info["url"]
2600
2571
  if strict and not url:
2601
2572
  raise IsADirectoryError(
2602
- errno.EISDIR,
2573
+ EISDIR,
2603
2574
  f"{fid} is a directory, with response {resp}",
2604
2575
  )
2605
2576
  return P115URL(
@@ -2613,7 +2584,7 @@ class P115OpenClient(ClientRequestMixin):
2613
2584
  headers=resp["headers"],
2614
2585
  )
2615
2586
  raise FileNotFoundError(
2616
- errno.ENOENT,
2587
+ ENOENT,
2617
2588
  f"no such pickcode: {pickcode!r}, with response {resp}",
2618
2589
  )
2619
2590
  if async_:
@@ -3506,44 +3477,6 @@ class P115OpenClient(ClientRequestMixin):
3506
3477
  api = complete_proapi("/open/upload/resume", base_url)
3507
3478
  return self.request(url=api, method="POST", data=payload, async_=async_, **request_kwargs)
3508
3479
 
3509
- @overload
3510
- def user_info(
3511
- self,
3512
- /,
3513
- base_url: bool | str | Callable[[], str] = False,
3514
- *,
3515
- async_: Literal[False] = False,
3516
- **request_kwargs,
3517
- ) -> dict:
3518
- ...
3519
- @overload
3520
- def user_info(
3521
- self,
3522
- /,
3523
- base_url: bool | str | Callable[[], str] = False,
3524
- *,
3525
- async_: Literal[True],
3526
- **request_kwargs,
3527
- ) -> Coroutine[Any, Any, dict]:
3528
- ...
3529
- def user_info(
3530
- self,
3531
- /,
3532
- base_url: bool | str | Callable[[], str] = False,
3533
- *,
3534
- async_: Literal[False, True] = False,
3535
- **request_kwargs,
3536
- ) -> dict | Coroutine[Any, Any, dict]:
3537
- """获取用户信息
3538
-
3539
- GET https://proapi.115.com/open/user/info
3540
-
3541
- .. note::
3542
- https://www.yuque.com/115yun/open/ot1litggzxa1czww
3543
- """
3544
- api = complete_proapi("/open/user/info", base_url)
3545
- return self.request(url=api, async_=async_, **request_kwargs)
3546
-
3547
3480
  @overload
3548
3481
  def upload_file_init(
3549
3482
  self,
@@ -3779,6 +3712,8 @@ class P115OpenClient(ClientRequestMixin):
3779
3712
  async_=async_, # type: ignore
3780
3713
  **request_kwargs,
3781
3714
  )
3715
+ if filesize == 0:
3716
+ filesha1 = "DA39A3EE5E6B4B0D3255BFEF95601890AFD80709"
3782
3717
  need_calc_filesha1 = not filesha1 and multipart_resume_data is None
3783
3718
  read_range_bytes_or_hash: None | Callable = None
3784
3719
  try:
@@ -3787,7 +3722,9 @@ class P115OpenClient(ClientRequestMixin):
3787
3722
  pass
3788
3723
  if isinstance(file, Buffer):
3789
3724
  filesize = buffer_length(file)
3790
- if need_calc_filesha1:
3725
+ if filesize == 0:
3726
+ filesha1 = "DA39A3EE5E6B4B0D3255BFEF95601890AFD80709"
3727
+ elif need_calc_filesha1:
3791
3728
  filesha1 = sha1(file).hexdigest()
3792
3729
  if multipart_resume_data is None and filesize >= 1 << 20:
3793
3730
  view = memoryview(file)
@@ -3840,7 +3777,9 @@ class P115OpenClient(ClientRequestMixin):
3840
3777
  filesize = (yield seek(0, 2)) - curpos
3841
3778
  finally:
3842
3779
  yield seek(curpos)
3843
- if multipart_resume_data is None and filesize >= 1 << 20:
3780
+ if filesize == 0:
3781
+ filesha1 = "DA39A3EE5E6B4B0D3255BFEF95601890AFD80709"
3782
+ elif multipart_resume_data is None and filesize >= 1 << 20:
3844
3783
  read: Callable[[int], Buffer] | Callable[[int], Awaitable[Buffer]]
3845
3784
  if seekable:
3846
3785
  if async_:
@@ -3897,16 +3836,24 @@ class P115OpenClient(ClientRequestMixin):
3897
3836
  url = cast(str, multipart_resume_data.get("url", ""))
3898
3837
  if not url:
3899
3838
  url = self.upload_endpoint_url(bucket, object)
3900
- token = multipart_resume_data.get("token")
3901
- if not token:
3902
- token = self.upload_token
3839
+ callback_var = loads(multipart_resume_data["callback"]["callback_var"])
3840
+ yield self.upload_resume_open(
3841
+ {
3842
+ "fileid": object,
3843
+ "file_size": multipart_resume_data["filesize"],
3844
+ "target": callback_var["x:target"],
3845
+ "pick_code": callback_var["x:pick_code"],
3846
+ },
3847
+ async_=async_,
3848
+ **request_kwargs,
3849
+ )
3903
3850
  return oss_multipart_upload(
3904
3851
  self.request,
3905
3852
  file, # type: ignore
3906
3853
  url=url,
3907
3854
  bucket=bucket,
3908
3855
  object=object,
3909
- token=token,
3856
+ token=self.upload_token,
3910
3857
  callback=multipart_resume_data["callback"],
3911
3858
  upload_id=multipart_resume_data["upload_id"],
3912
3859
  partsize=multipart_resume_data["partsize"],
@@ -3941,7 +3888,7 @@ class P115OpenClient(ClientRequestMixin):
3941
3888
  case 1:
3942
3889
  bucket, object, callback = data["bucket"], data["object"], data["callback"]
3943
3890
  case _:
3944
- raise P115OSError(errno.EINVAL, resp)
3891
+ raise P115OSError(EINVAL, resp)
3945
3892
  url = self.upload_endpoint_url(bucket, object)
3946
3893
  token = self.upload_token
3947
3894
  if partsize <= 0:
@@ -3976,6 +3923,44 @@ class P115OpenClient(ClientRequestMixin):
3976
3923
  )
3977
3924
  return run_gen_step(gen_step, async_=async_)
3978
3925
 
3926
+ @overload
3927
+ def user_info(
3928
+ self,
3929
+ /,
3930
+ base_url: bool | str | Callable[[], str] = False,
3931
+ *,
3932
+ async_: Literal[False] = False,
3933
+ **request_kwargs,
3934
+ ) -> dict:
3935
+ ...
3936
+ @overload
3937
+ def user_info(
3938
+ self,
3939
+ /,
3940
+ base_url: bool | str | Callable[[], str] = False,
3941
+ *,
3942
+ async_: Literal[True],
3943
+ **request_kwargs,
3944
+ ) -> Coroutine[Any, Any, dict]:
3945
+ ...
3946
+ def user_info(
3947
+ self,
3948
+ /,
3949
+ base_url: bool | str | Callable[[], str] = False,
3950
+ *,
3951
+ async_: Literal[False, True] = False,
3952
+ **request_kwargs,
3953
+ ) -> dict | Coroutine[Any, Any, dict]:
3954
+ """获取用户信息
3955
+
3956
+ GET https://proapi.115.com/open/user/info
3957
+
3958
+ .. note::
3959
+ https://www.yuque.com/115yun/open/ot1litggzxa1czww
3960
+ """
3961
+ api = complete_proapi("/open/user/info", base_url)
3962
+ return self.request(url=api, async_=async_, **request_kwargs)
3963
+
3979
3964
  download_url_open = download_url
3980
3965
  download_url_info_open = download_url_info
3981
3966
  fs_copy_open = fs_copy
@@ -4000,6 +3985,13 @@ class P115OpenClient(ClientRequestMixin):
4000
3985
  class P115Client(P115OpenClient):
4001
3986
  """115 的客户端对象
4002
3987
 
3988
+ .. note::
3989
+ 目前允许 1 个用户同时登录多个开放平台应用(用 AppID 区别),但如果多次登录同 1 个应用,则只有最近登录的有效
3990
+
3991
+ 目前不允许短时间内再次用 `refresh_token` 刷新 `access_token`,但你可以用登录的方式再次授权登录以获取 `access_token`,即可不受频率限制
3992
+
3993
+ 1 个 `refresh_token` 只能使用 1 次,可获取新的 `refresh_token` 和 `access_token`,如果请求刷新时,发送成功但读取失败,可能导致 `refresh_token` 报废,这时需要重新授权登录
3994
+
4003
3995
  :param cookies: 115 的 cookies,要包含 `UID`、`CID`、`KID` 和 `SEID` 等
4004
3996
 
4005
3997
  - 如果是 None,则会要求人工扫二维码登录
@@ -4925,14 +4917,14 @@ class P115Client(P115OpenClient):
4925
4917
  data = resp["data"]
4926
4918
  if replace is False:
4927
4919
  inst: P115OpenClient | Self = P115OpenClient.from_token(data["access_token"], data["refresh_token"])
4928
- inst.app_id = app_id
4929
4920
  else:
4930
4921
  if replace is True:
4931
4922
  inst = self
4932
4923
  else:
4933
4924
  inst = replace
4934
4925
  inst.refresh_token = data["refresh_token"]
4935
- setattr(inst, "access_token", data["access_token"])
4926
+ inst.access_token = data["access_token"]
4927
+ inst.app_id = app_id
4936
4928
  return inst
4937
4929
  return run_gen_step(gen_step, async_=async_)
4938
4930
 
@@ -5210,10 +5202,14 @@ class P115Client(P115OpenClient):
5210
5202
  elif data is not None:
5211
5203
  request_kwargs["data"] = data
5212
5204
  request_kwargs.setdefault("parse", default_parse)
5213
- if url.startswith("https://proapi.115.com/open/"):
5205
+ use_cookies = not url.startswith("https://proapi.115.com/open/")
5206
+ if not use_cookies:
5214
5207
  headers["cookie"] = ""
5215
- return request(url=url, method=method, **request_kwargs)
5216
5208
  def gen_step():
5209
+ if async_:
5210
+ lock: Lock | AsyncLock = self.request_alock
5211
+ else:
5212
+ lock = self.request_lock
5217
5213
  check_for_relogin = self.check_for_relogin
5218
5214
  cant_relogin = not callable(check_for_relogin)
5219
5215
  if get_cookies is not None:
@@ -5223,59 +5219,86 @@ class P115Client(P115OpenClient):
5223
5219
  for i in count(0):
5224
5220
  exc = None
5225
5221
  try:
5226
- if get_cookies is None:
5227
- if need_set_cookies:
5228
- cookies_old = headers["cookie"] = self.cookies_str
5229
- else:
5230
- if get_cookies_need_arg:
5231
- cookies_ = yield get_cookies(async_)
5222
+ if use_cookies:
5223
+ if get_cookies is None:
5224
+ if need_set_cookies:
5225
+ cookies_old = headers["cookie"] = self.cookies_str
5232
5226
  else:
5233
- cookies_ = yield get_cookies()
5234
- if not cookies_:
5235
- raise ValueError("can't get new cookies")
5236
- headers["cookie"] = cookies_
5237
- return partial(request, url=url, method=method, **request_kwargs)
5227
+ if get_cookies_need_arg:
5228
+ cookies_ = yield get_cookies(async_)
5229
+ else:
5230
+ cookies_ = yield get_cookies()
5231
+ if not cookies_:
5232
+ raise ValueError("can't get new cookies")
5233
+ headers["cookie"] = cookies_
5234
+ resp = yield partial(request, url=url, method=method, **request_kwargs)
5235
+ return resp
5238
5236
  except BaseException as e:
5239
5237
  exc = e
5240
- if cant_relogin or not need_set_cookies:
5238
+ if cant_relogin or use_cookies and not need_set_cookies:
5241
5239
  raise
5242
5240
  if isinstance(e, (AuthenticationError, LoginError)):
5243
- if get_cookies is not None or cookies_old != self.cookies_str or cookies_old != self._read_cookies():
5241
+ if use_cookies and (
5242
+ get_cookies is not None or
5243
+ cookies_old != self.cookies_str or
5244
+ cookies_old != self._read_cookies()
5245
+ ):
5244
5246
  continue
5245
5247
  raise
5246
5248
  res = yield partial(cast(Callable, check_for_relogin), e)
5247
5249
  if not res if isinstance(res, bool) else res != 405:
5248
5250
  raise
5249
- if get_cookies is not None:
5250
- continue
5251
- cookies = self.cookies_str
5252
- if not cookies_equal(cookies, cookies_old):
5253
- continue
5254
- cookies_mtime = getattr(self, "cookies_mtime", 0)
5255
- if async_:
5256
- lock: Lock | AsyncLock = self.request_alock
5257
- yield lock.acquire()
5251
+ if use_cookies:
5252
+ if get_cookies is not None:
5253
+ continue
5254
+ cookies = self.cookies_str
5255
+ if not cookies_equal(cookies, cookies_old):
5256
+ continue
5257
+ cookies_mtime = getattr(self, "cookies_mtime", 0)
5258
+ yield lock.acquire
5259
+ try:
5260
+ cookies_new = self.cookies_str
5261
+ cookies_mtime_new = getattr(self, "cookies_mtime", 0)
5262
+ if cookies_equal(cookies, cookies_new):
5263
+ m = CRE_COOKIES_UID_search(cookies)
5264
+ uid = "" if m is None else m[0]
5265
+ need_read_cookies = cookies_mtime_new > cookies_mtime
5266
+ if need_read_cookies:
5267
+ cookies_new = self._read_cookies()
5268
+ if i and cookies_equal(cookies_old, cookies_new):
5269
+ raise
5270
+ if not (need_read_cookies and cookies_new):
5271
+ warn(f"relogin to refresh cookies: UID={uid!r} app={self.login_app()!r}", category=P115Warning)
5272
+ yield self.login_another_app(
5273
+ replace=True,
5274
+ async_=async_, # type: ignore
5275
+ )
5276
+ finally:
5277
+ lock.release()
5258
5278
  else:
5259
- lock = self.request_lock
5260
- lock.acquire()
5261
- try:
5262
- cookies_new = self.cookies_str
5263
- cookies_mtime_new = getattr(self, "cookies_mtime", 0)
5264
- if cookies_equal(cookies, cookies_new):
5265
- m = CRE_COOKIES_UID_search(cookies)
5266
- uid = "" if m is None else m[0]
5267
- need_read_cookies = cookies_mtime_new > cookies_mtime
5268
- if need_read_cookies:
5269
- cookies_new = self._read_cookies()
5270
- if i and cookies_equal(cookies_old, cookies_new):
5271
- raise
5272
- if not (need_read_cookies and cookies_new):
5273
- warn(f"relogin to refresh cookies: UID={uid!r} app={self.login_app()!r}", category=P115Warning)
5274
- yield self.login_another_app(replace=True, async_=async_) # type: ignore
5275
- finally:
5276
- lock.release()
5279
+ access_token = self.access_token
5280
+ yield lock.acquire
5281
+ try:
5282
+ if access_token != self.access_token:
5283
+ continue
5284
+ if hasattr(self, "app_id"):
5285
+ app_id = self.app_id
5286
+ yield self.login_another_open(
5287
+ app_id,
5288
+ replace=True,
5289
+ async_=async_, # type: ignore
5290
+ )
5291
+ warn(f"relogin to refresh token: {app_id=}", category=P115Warning)
5292
+ else:
5293
+ resp = yield self.refresh_access_token(
5294
+ async_=async_, # type: ignore
5295
+ )
5296
+ check_response(resp)
5297
+ warn("relogin to refresh token (using refresh_token)", category=P115Warning)
5298
+ finally:
5299
+ lock.release()
5277
5300
  finally:
5278
- if (cookies_ and
5301
+ if (use_cookies and cookies_ and
5279
5302
  get_cookies is not None and
5280
5303
  revert_cookies is not None and (
5281
5304
  not exc or not (
@@ -6314,7 +6337,7 @@ class P115Client(P115OpenClient):
6314
6337
  url = info["url"]
6315
6338
  if strict and not url:
6316
6339
  raise IsADirectoryError(
6317
- errno.EISDIR,
6340
+ EISDIR,
6318
6341
  f"{fid} is a directory, with response {resp}",
6319
6342
  )
6320
6343
  return P115URL(
@@ -6328,7 +6351,7 @@ class P115Client(P115OpenClient):
6328
6351
  headers=resp["headers"],
6329
6352
  )
6330
6353
  raise FileNotFoundError(
6331
- errno.ENOENT,
6354
+ ENOENT,
6332
6355
  f"no such pickcode: {pickcode!r}, with response {resp}",
6333
6356
  )
6334
6357
  if async_:
@@ -9245,6 +9268,53 @@ class P115Client(P115OpenClient):
9245
9268
  payload["custom_order"] = 1
9246
9269
  return self.request(url=api, params=payload, async_=async_, **request_kwargs)
9247
9270
 
9271
+ @overload
9272
+ def fs_files_blank_document(
9273
+ self,
9274
+ payload: str | dict,
9275
+ /,
9276
+ base_url: bool | str | Callable[[], str] = False,
9277
+ *,
9278
+ async_: Literal[False] = False,
9279
+ **request_kwargs,
9280
+ ) -> dict:
9281
+ ...
9282
+ @overload
9283
+ def fs_files_blank_document(
9284
+ self,
9285
+ payload: str | dict,
9286
+ /,
9287
+ base_url: bool | str | Callable[[], str] = False,
9288
+ *,
9289
+ async_: Literal[True],
9290
+ **request_kwargs,
9291
+ ) -> Coroutine[Any, Any, dict]:
9292
+ ...
9293
+ def fs_files_blank_document(
9294
+ self,
9295
+ payload: str | dict,
9296
+ /,
9297
+ base_url: bool | str | Callable[[], str] = False,
9298
+ *,
9299
+ async_: Literal[False, True] = False,
9300
+ **request_kwargs,
9301
+ ) -> dict | Coroutine[Any, Any, dict]:
9302
+ """新建空白 office 文件
9303
+
9304
+ POST https://webapi.115.com/files/blank_document
9305
+
9306
+ :payload:
9307
+ - file_name: str 💡 文件名,不含后缀
9308
+ - pid: int | str = 0 💡 目录 id
9309
+ - type: 1 | 2 | 3 = 1 💡 1:Word文档(.docx) 2:Excel表格(.xlsx) 3:PPT文稿(.pptx)
9310
+ """
9311
+ api = complete_webapi("/files/blank_document", base_url=base_url)
9312
+ if isinstance(payload, str):
9313
+ payload = {"pid": 0, "type": 1, "file_name": payload}
9314
+ else:
9315
+ payload = {"pid": 0, "type": 1, **payload}
9316
+ return self.request(url=api, method="POST", data=payload, async_=async_, **request_kwargs)
9317
+
9248
9318
  @overload
9249
9319
  def fs_files_history(
9250
9320
  self,
@@ -9329,7 +9399,7 @@ class P115Client(P115OpenClient):
9329
9399
  POST https://webapi.115.com/files/history
9330
9400
 
9331
9401
  :payload:
9332
- - pick_code: str 💡 视频的提取码
9402
+ - pick_code: str 💡 文件的提取码
9333
9403
  - op: str = "update" 💡 操作类型,具体有哪些还需要再研究
9334
9404
  - category: int = <default>
9335
9405
  - definition: int = <default> 💡 视频清晰度
@@ -17193,13 +17263,13 @@ class P115Client(P115OpenClient):
17193
17263
  file_id = payload["file_id"]
17194
17264
  if not info:
17195
17265
  raise FileNotFoundError(
17196
- errno.ENOENT,
17266
+ ENOENT,
17197
17267
  f"no such id: {file_id!r}, with response {resp}",
17198
17268
  )
17199
17269
  url = info["url"]
17200
17270
  if strict and not url:
17201
17271
  raise IsADirectoryError(
17202
- errno.EISDIR,
17272
+ EISDIR,
17203
17273
  f"{file_id} is a directory, with response {resp}",
17204
17274
  )
17205
17275
  return P115URL(
@@ -17945,13 +18015,13 @@ class P115Client(P115OpenClient):
17945
18015
  file_id = payload["file_id"]
17946
18016
  if not info:
17947
18017
  raise FileNotFoundError(
17948
- errno.ENOENT,
18018
+ ENOENT,
17949
18019
  f"no such id: {file_id!r}, with response {resp}",
17950
18020
  )
17951
18021
  url = info["url"]
17952
18022
  if strict and not url:
17953
18023
  raise IsADirectoryError(
17954
- errno.EISDIR,
18024
+ EISDIR,
17955
18025
  f"{file_id} is a directory, with response {resp}",
17956
18026
  )
17957
18027
  return P115URL(
@@ -19166,8 +19236,6 @@ class P115Client(P115OpenClient):
19166
19236
 
19167
19237
  :return: 接口响应
19168
19238
  """
19169
- if filesize >= 1 << 20 and read_range_bytes_or_hash is None:
19170
- raise ValueError("filesize >= 1 MB, thus need pass the `read_range_bytes_or_hash` argument")
19171
19239
  filesha1 = filesha1.upper()
19172
19240
  target = f"U_1_{pid}"
19173
19241
  def gen_step():
@@ -19353,8 +19421,6 @@ class P115Client(P115OpenClient):
19353
19421
  )
19354
19422
  return run_gen_step(gen_step, async_=async_)
19355
19423
 
19356
- # TODO: 当文件 < 1 MB 时,文件不急着打开,需要时再打开
19357
- # TODO: 对于上传空文件,有特别的速度(sha1写死)
19358
19424
  @overload # type: ignore
19359
19425
  def upload_file(
19360
19426
  self,
@@ -19493,11 +19559,9 @@ class P115Client(P115OpenClient):
19493
19559
  async_=async_, # type: ignore
19494
19560
  **request_kwargs,
19495
19561
  )
19496
- need_calc_filesha1 = (
19497
- not filesha1 and
19498
- not upload_directly and
19499
- multipart_resume_data is None
19500
- )
19562
+ if filesize == 0:
19563
+ filesha1 = "DA39A3EE5E6B4B0D3255BFEF95601890AFD80709"
19564
+ need_calc_filesha1 = not filesha1 and not upload_directly and multipart_resume_data is None
19501
19565
  read_range_bytes_or_hash: None | Callable = None
19502
19566
  try:
19503
19567
  file = getattr(file, "getbuffer")()
@@ -19505,7 +19569,9 @@ class P115Client(P115OpenClient):
19505
19569
  pass
19506
19570
  if isinstance(file, Buffer):
19507
19571
  filesize = buffer_length(file)
19508
- if need_calc_filesha1:
19572
+ if filesize == 0:
19573
+ filesha1 = "DA39A3EE5E6B4B0D3255BFEF95601890AFD80709"
19574
+ elif need_calc_filesha1:
19509
19575
  filesha1 = sha1(file).hexdigest()
19510
19576
  if not upload_directly and multipart_resume_data is None and filesize >= 1 << 20:
19511
19577
  view = memoryview(file)
@@ -19558,7 +19624,9 @@ class P115Client(P115OpenClient):
19558
19624
  filesize = (yield seek(0, 2)) - curpos
19559
19625
  finally:
19560
19626
  yield seek(curpos)
19561
- if not upload_directly and multipart_resume_data is None and filesize >= 1 << 20:
19627
+ if filesize == 0:
19628
+ filesha1 = "DA39A3EE5E6B4B0D3255BFEF95601890AFD80709"
19629
+ elif not upload_directly and multipart_resume_data is None and filesize >= 1 << 20:
19562
19630
  read: Callable[[int], Buffer] | Callable[[int], Awaitable[Buffer]]
19563
19631
  if seekable:
19564
19632
  if async_:
@@ -19615,16 +19683,24 @@ class P115Client(P115OpenClient):
19615
19683
  url = cast(str, multipart_resume_data.get("url", ""))
19616
19684
  if not url:
19617
19685
  url = self.upload_endpoint_url(bucket, object)
19618
- token = multipart_resume_data.get("token")
19619
- if not token:
19620
- token = self.upload_token
19686
+ callback_var = loads(multipart_resume_data["callback"]["callback_var"])
19687
+ yield self.upload_resume(
19688
+ {
19689
+ "fileid": object,
19690
+ "filesize": multipart_resume_data["filesize"],
19691
+ "target": callback_var["x:target"],
19692
+ "pickcode": callback_var["x:pick_code"],
19693
+ },
19694
+ async_=async_,
19695
+ **request_kwargs,
19696
+ )
19621
19697
  return oss_multipart_upload(
19622
19698
  self.request,
19623
19699
  file, # type: ignore
19624
19700
  url=url,
19625
19701
  bucket=bucket,
19626
19702
  object=object,
19627
- token=token,
19703
+ token=self.upload_token,
19628
19704
  callback=multipart_resume_data["callback"],
19629
19705
  upload_id=multipart_resume_data["upload_id"],
19630
19706
  partsize=multipart_resume_data["partsize"],
@@ -19669,7 +19745,7 @@ class P115Client(P115OpenClient):
19669
19745
  elif status == 1 and statuscode == 0:
19670
19746
  bucket, object, callback = resp["bucket"], resp["object"], resp["callback"]
19671
19747
  else:
19672
- raise P115OSError(errno.EINVAL, resp)
19748
+ raise P115OSError(EINVAL, resp)
19673
19749
  url = self.upload_endpoint_url(bucket, object)
19674
19750
  token = self.upload_token
19675
19751
  if partsize <= 0:
@@ -19819,11 +19895,10 @@ class P115Client(P115OpenClient):
19819
19895
  return self.request(url=api, async_=async_, **request_kwargs)
19820
19896
 
19821
19897
  @overload # type: ignore
19822
- @staticmethod
19823
19898
  def user_info(
19824
- payload: int | str | dict,
19899
+ self: int | str | dict | P115Client,
19900
+ payload: None | int | str | dict = None,
19825
19901
  /,
19826
- request: None | Callable = None,
19827
19902
  base_url: bool | str | Callable[[], str] = False,
19828
19903
  *,
19829
19904
  async_: Literal[False] = False,
@@ -19831,22 +19906,20 @@ class P115Client(P115OpenClient):
19831
19906
  ) -> dict:
19832
19907
  ...
19833
19908
  @overload
19834
- @staticmethod
19835
19909
  def user_info(
19836
- payload: int | str | dict,
19910
+ self: int | str | dict | P115Client,
19911
+ payload: None | int | str | dict = None,
19837
19912
  /,
19838
- request: None | Callable = None,
19839
19913
  base_url: bool | str | Callable[[], str] = False,
19840
19914
  *,
19841
19915
  async_: Literal[True],
19842
19916
  **request_kwargs,
19843
19917
  ) -> Coroutine[Any, Any, dict]:
19844
19918
  ...
19845
- @staticmethod
19846
19919
  def user_info(
19847
- payload: int | str | dict,
19920
+ self: int | str | dict | P115Client,
19921
+ payload: None | int | str | dict = None,
19848
19922
  /,
19849
- request: None | Callable = None,
19850
19923
  base_url: bool | str | Callable[[], str] = False,
19851
19924
  *,
19852
19925
  async_: Literal[False, True] = False,
@@ -19856,19 +19929,31 @@ class P115Client(P115OpenClient):
19856
19929
 
19857
19930
  GET https://my.115.com/proapi/3.0/index.php?method=user_info
19858
19931
 
19932
+ .. important::
19933
+ 这个函数可以作为 staticmethod 使用,只要 `self` 不是 P115Client 类型,此时不需要登录
19934
+
19859
19935
  :payload:
19860
19936
  - uid: int | str
19861
19937
  """
19862
19938
  api = complete_api("/proapi/3.0/index.php", "my", base_url=base_url)
19939
+ if isinstance(self, P115Client):
19940
+ if payload is None:
19941
+ payload = self.user_id
19942
+ else:
19943
+ payload = self
19863
19944
  if isinstance(payload, (int, str)):
19864
19945
  payload = {"uid": payload, "method": "user_info"}
19865
19946
  else:
19866
19947
  payload = {"method": "user_info", **payload}
19867
- request_kwargs.setdefault("parse", default_parse)
19868
- if request is None:
19869
- return get_default_request()(url=api, params=payload, async_=async_, **request_kwargs)
19948
+ if isinstance(self, P115Client):
19949
+ return self.request(url=api, params=payload, async_=async_, **request_kwargs)
19870
19950
  else:
19871
- return request(url=api, params=payload, **request_kwargs)
19951
+ request_kwargs.setdefault("parse", default_parse)
19952
+ request = request_kwargs.pop("request", None)
19953
+ if request is None:
19954
+ return get_default_request()(url=api, params=payload, async_=async_, **request_kwargs)
19955
+ else:
19956
+ return request(url=api, params=payload, **request_kwargs)
19872
19957
 
19873
19958
  @overload
19874
19959
  def user_my(
@@ -20401,6 +20486,49 @@ class P115Client(P115OpenClient):
20401
20486
  api = complete_proapi("/vip/check_spw", base_url, app)
20402
20487
  return self.request(url=api, async_=async_, **request_kwargs)
20403
20488
 
20489
+ @overload
20490
+ def user_vip_limit(
20491
+ self,
20492
+ payload: int | dict = 2,
20493
+ /,
20494
+ base_url: bool | str | Callable[[], str] = False,
20495
+ *,
20496
+ async_: Literal[False] = False,
20497
+ **request_kwargs,
20498
+ ) -> dict:
20499
+ ...
20500
+ @overload
20501
+ def user_vip_limit(
20502
+ self,
20503
+ payload: int | dict = 2,
20504
+ /,
20505
+ base_url: bool | str | Callable[[], str] = False,
20506
+ *,
20507
+ async_: Literal[True],
20508
+ **request_kwargs,
20509
+ ) -> Coroutine[Any, Any, dict]:
20510
+ ...
20511
+ def user_vip_limit(
20512
+ self,
20513
+ payload: int | dict = 2,
20514
+ /,
20515
+ base_url: bool | str | Callable[[], str] = False,
20516
+ *,
20517
+ async_: Literal[False, True] = False,
20518
+ **request_kwargs,
20519
+ ) -> dict | Coroutine[Any, Any, dict]:
20520
+ """获取 vip 的某些限制
20521
+
20522
+ GET https://webapi.115.com/user/vip_limit
20523
+
20524
+ :payload:
20525
+ - feature: int = 2
20526
+ """
20527
+ api = complete_webapi("/user/vip_limit", base_url=base_url)
20528
+ if isinstance(payload, int):
20529
+ payload = {"feature": payload}
20530
+ return self.request(url=api, params=payload, async_=async_, **request_kwargs)
20531
+
20404
20532
  ########## User Share API ##########
20405
20533
 
20406
20534
  @overload