p115client 0.0.5.12__tar.gz → 0.0.5.12.1__tar.gz

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.
Files changed (27) hide show
  1. {p115client-0.0.5.12 → p115client-0.0.5.12.1}/PKG-INFO +1 -1
  2. {p115client-0.0.5.12 → p115client-0.0.5.12.1}/p115client/client.py +156 -48
  3. {p115client-0.0.5.12 → p115client-0.0.5.12.1}/p115client/const.py +7 -4
  4. {p115client-0.0.5.12 → p115client-0.0.5.12.1}/p115client/tool/offline.py +39 -7
  5. {p115client-0.0.5.12 → p115client-0.0.5.12.1}/pyproject.toml +1 -1
  6. {p115client-0.0.5.12 → p115client-0.0.5.12.1}/LICENSE +0 -0
  7. {p115client-0.0.5.12 → p115client-0.0.5.12.1}/p115client/__init__.py +0 -0
  8. {p115client-0.0.5.12 → p115client-0.0.5.12.1}/p115client/_upload.py +0 -0
  9. {p115client-0.0.5.12 → p115client-0.0.5.12.1}/p115client/exception.py +0 -0
  10. {p115client-0.0.5.12 → p115client-0.0.5.12.1}/p115client/py.typed +0 -0
  11. {p115client-0.0.5.12 → p115client-0.0.5.12.1}/p115client/tool/__init__.py +0 -0
  12. {p115client-0.0.5.12 → p115client-0.0.5.12.1}/p115client/tool/attr.py +0 -0
  13. {p115client-0.0.5.12 → p115client-0.0.5.12.1}/p115client/tool/auth.py +0 -0
  14. {p115client-0.0.5.12 → p115client-0.0.5.12.1}/p115client/tool/download.py +0 -0
  15. {p115client-0.0.5.12 → p115client-0.0.5.12.1}/p115client/tool/edit.py +0 -0
  16. {p115client-0.0.5.12 → p115client-0.0.5.12.1}/p115client/tool/export_dir.py +0 -0
  17. {p115client-0.0.5.12 → p115client-0.0.5.12.1}/p115client/tool/fs_files.py +0 -0
  18. {p115client-0.0.5.12 → p115client-0.0.5.12.1}/p115client/tool/history.py +0 -0
  19. {p115client-0.0.5.12 → p115client-0.0.5.12.1}/p115client/tool/iterdir.py +0 -0
  20. {p115client-0.0.5.12 → p115client-0.0.5.12.1}/p115client/tool/life.py +0 -0
  21. {p115client-0.0.5.12 → p115client-0.0.5.12.1}/p115client/tool/pool.py +0 -0
  22. {p115client-0.0.5.12 → p115client-0.0.5.12.1}/p115client/tool/request.py +0 -0
  23. {p115client-0.0.5.12 → p115client-0.0.5.12.1}/p115client/tool/upload.py +0 -0
  24. {p115client-0.0.5.12 → p115client-0.0.5.12.1}/p115client/tool/util.py +0 -0
  25. {p115client-0.0.5.12 → p115client-0.0.5.12.1}/p115client/tool/xys.py +0 -0
  26. {p115client-0.0.5.12 → p115client-0.0.5.12.1}/p115client/type.py +0 -0
  27. {p115client-0.0.5.12 → p115client-0.0.5.12.1}/readme.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: p115client
3
- Version: 0.0.5.12
3
+ Version: 0.0.5.12.1
4
4
  Summary: Python 115 webdisk client.
5
5
  Home-page: https://github.com/ChenyangGao/p115client
6
6
  License: MIT
@@ -15,6 +15,7 @@ from collections.abc import (
15
15
  AsyncGenerator, AsyncIterable, Awaitable, Buffer, Callable, Coroutine, Generator,
16
16
  ItemsView, Iterable, Iterator, Mapping, MutableMapping, Sequence,
17
17
  )
18
+ from contextlib import contextmanager
18
19
  from datetime import date, datetime, timedelta
19
20
  from functools import partial
20
21
  from hashlib import md5, sha1
@@ -30,11 +31,12 @@ from platform import system
30
31
  from posixpath import splitext
31
32
  from re import compile as re_compile, MULTILINE
32
33
  from string import digits
34
+ from sys import _getframe
33
35
  from tempfile import TemporaryFile
34
36
  from threading import Lock
35
37
  from time import time
36
38
  from typing import cast, overload, Any, Final, Literal, Self, Unpack
37
- from urllib.parse import quote, unquote, urlencode, urlsplit, urlunsplit
39
+ from urllib.parse import parse_qsl, quote, unquote, urlencode, urlsplit, urlunsplit
38
40
  from uuid import uuid4
39
41
  from warnings import warn
40
42
 
@@ -63,7 +65,10 @@ from startfile import startfile, startfile_async # type: ignore
63
65
  from undefined import undefined
64
66
  from yarl import URL
65
67
 
66
- from .const import CLASS_TO_TYPE, CLIENT_API_MAP, SSOENT_TO_APP, SUFFIX_TO_TYPE, errno
68
+ from .const import (
69
+ CLASS_TO_TYPE, CLIENT_API_METHODS_MAP, CLIENT_METHOD_API_MAP,
70
+ SSOENT_TO_APP, SUFFIX_TO_TYPE, errno,
71
+ )
67
72
  from .exception import (
68
73
  AccessTokenError, AuthenticationError, BusyOSError, DataError, LoginError,
69
74
  OpenAppAuthLimitExceeded, NotSupportedError, P115OSError, OperationalError,
@@ -74,7 +79,6 @@ from ._upload import buffer_length, make_dataiter, oss_upload, oss_multipart_upl
74
79
 
75
80
 
76
81
  CRE_SET_COOKIE: Final = re_compile(r"[0-9a-f]{32}=[0-9a-f]{32}.*")
77
- CRE_CLIENT_API_search: Final = re_compile(r"^ +((?:GET|POST) .*)", MULTILINE).search
78
82
  CRE_COOKIES_UID_search: Final = re_compile(r"(?<=\bUID=)[^\s;]+").search
79
83
  CRE_API_match: Final = re_compile(r"http://(web|pro)api.115.com(?=/|\?|#|$)").match
80
84
  ED2K_NAME_TRANSTAB: Final = dict(zip(b"/|", ("%2F", "%7C")))
@@ -229,6 +233,20 @@ def try_parse_int(
229
233
  return int(s)
230
234
 
231
235
 
236
+ @contextmanager
237
+ def temp_globals(f_globals: None | dict = None, /, **ns):
238
+ if f_globals is None:
239
+ f_globals = _getframe(2).f_globals
240
+ old_globals = f_globals.copy()
241
+ if ns:
242
+ f_globals.update(ns)
243
+ try:
244
+ yield f_globals
245
+ finally:
246
+ f_globals.clear()
247
+ f_globals.update(old_globals)
248
+
249
+
232
250
  def json_loads(content: Buffer, /):
233
251
  try:
234
252
  if isinstance(content, (bytes, bytearray, memoryview)):
@@ -1573,6 +1591,19 @@ class ClientRequestMixin:
1573
1591
  """
1574
1592
  api = complete_api("/open/authorize", base_url=base_url)
1575
1593
  payload = {"response_type": "code", **payload}
1594
+ def parse(resp, content, /):
1595
+ if resp.status_code == 302:
1596
+ return {
1597
+ "state": True,
1598
+ "url": resp.headers["location"],
1599
+ "data": dict(parse_qsl(urlsplit(resp.headers["location"]).query)),
1600
+ "headers": dict(resp.headers),
1601
+ }
1602
+ else:
1603
+ return json_loads(content)
1604
+ request_kwargs["parse"] = parse
1605
+ for key in ("allow_redirects", "follow_redirects", "redirect"):
1606
+ request_kwargs[key] = False
1576
1607
  return self.request(url=api, params=payload, async_=async_, **request_kwargs)
1577
1608
 
1578
1609
  @overload
@@ -2615,7 +2646,6 @@ class ClientRequestMixin:
2615
2646
 
2616
2647
  :return: 文件的 ed2k 链接
2617
2648
  """
2618
- trantab = dict(zip(b"/|", ("%2F", "%7C")))
2619
2649
  if async_:
2620
2650
  async def request():
2621
2651
  async with self.open(url, headers=headers, async_=True) as file:
@@ -3481,6 +3511,7 @@ class P115OpenClient(ClientRequestMixin):
3481
3511
  - "user_ptime": 创建时间(无效,效果相当于 "user_utime")
3482
3512
  - "user_otime": 上一次打开时间(无效,效果相当于 "user_utime")
3483
3513
 
3514
+ - qid: int = <default>
3484
3515
  - r_all: 0 | 1 = <default>
3485
3516
  - record_open_time: 0 | 1 = 1 💡 是否要记录目录的打开时间
3486
3517
  - scid: int | str = <default>
@@ -3861,7 +3892,7 @@ class P115OpenClient(ClientRequestMixin):
3861
3892
  return self.fs_update(payload, async_=async_, **request_kwargs)
3862
3893
 
3863
3894
  @overload
3864
- def fs_video_history(
3895
+ def fs_video(
3865
3896
  self,
3866
3897
  payload: str | dict,
3867
3898
  /,
@@ -3872,7 +3903,7 @@ class P115OpenClient(ClientRequestMixin):
3872
3903
  ) -> dict:
3873
3904
  ...
3874
3905
  @overload
3875
- def fs_video_history(
3906
+ def fs_video(
3876
3907
  self,
3877
3908
  payload: str | dict,
3878
3909
  /,
@@ -3882,7 +3913,7 @@ class P115OpenClient(ClientRequestMixin):
3882
3913
  **request_kwargs,
3883
3914
  ) -> Coroutine[Any, Any, dict]:
3884
3915
  ...
3885
- def fs_video_history(
3916
+ def fs_video(
3886
3917
  self,
3887
3918
  payload: str | dict,
3888
3919
  /,
@@ -3891,24 +3922,28 @@ class P115OpenClient(ClientRequestMixin):
3891
3922
  async_: Literal[False, True] = False,
3892
3923
  **request_kwargs,
3893
3924
  ) -> dict | Coroutine[Any, Any, dict]:
3894
- """获取视频播放进度
3895
-
3896
- GET https://proapi.115.com/open/video/history
3925
+ """获取视频在线播放地址(和视频文件相关数据)
3897
3926
 
3927
+ GET https://proapi.115.com/open/video/play
3928
+
3898
3929
  .. admonition:: Reference
3899
3930
 
3900
- https://www.yuque.com/115yun/open/gssqdrsq6vfqigag
3931
+ https://www.yuque.com/115yun/open/hqglxv3cedi3p9dz
3932
+
3933
+ .. hint::
3934
+ 需切换音轨时,在请求返回的播放地址中增加请求参数 `&audio_track=${index}`,值就是接口响应中 `multitrack_list` 中某个成员的索引,从 0 开始计数
3901
3935
 
3902
3936
  :payload:
3903
3937
  - pick_code: str 💡 文件提取码
3938
+ - share_id: int | str = <default> 💡 共享 id,获取共享文件播放地址所需
3904
3939
  """
3905
- api = complete_proapi("/open/video/history", base_url)
3940
+ api = complete_proapi("/open/video/play", base_url)
3906
3941
  if isinstance(payload, str):
3907
3942
  payload = {"pick_code": payload}
3908
3943
  return self.request(url=api, params=payload, async_=async_, **request_kwargs)
3909
3944
 
3910
3945
  @overload
3911
- def fs_video_history_set(
3946
+ def fs_video_history(
3912
3947
  self,
3913
3948
  payload: str | dict,
3914
3949
  /,
@@ -3919,7 +3954,7 @@ class P115OpenClient(ClientRequestMixin):
3919
3954
  ) -> dict:
3920
3955
  ...
3921
3956
  @overload
3922
- def fs_video_history_set(
3957
+ def fs_video_history(
3923
3958
  self,
3924
3959
  payload: str | dict,
3925
3960
  /,
@@ -3929,7 +3964,7 @@ class P115OpenClient(ClientRequestMixin):
3929
3964
  **request_kwargs,
3930
3965
  ) -> Coroutine[Any, Any, dict]:
3931
3966
  ...
3932
- def fs_video_history_set(
3967
+ def fs_video_history(
3933
3968
  self,
3934
3969
  payload: str | dict,
3935
3970
  /,
@@ -3938,26 +3973,24 @@ class P115OpenClient(ClientRequestMixin):
3938
3973
  async_: Literal[False, True] = False,
3939
3974
  **request_kwargs,
3940
3975
  ) -> dict | Coroutine[Any, Any, dict]:
3941
- """记忆视频播放进度
3976
+ """获取视频播放进度
3942
3977
 
3943
- POST https://proapi.115.com/open/video/history
3978
+ GET https://proapi.115.com/open/video/history
3944
3979
 
3945
3980
  .. admonition:: Reference
3946
3981
 
3947
- https://www.yuque.com/115yun/open/bshagbxv1gzqglg4
3982
+ https://www.yuque.com/115yun/open/gssqdrsq6vfqigag
3948
3983
 
3949
3984
  :payload:
3950
3985
  - pick_code: str 💡 文件提取码
3951
- - time: int = <default> 💡 视频播放进度时长 (单位秒)
3952
- - watch_end: int = <default> 💡 视频是否播放播放完毕 0:未完毕 1:完毕
3953
3986
  """
3954
3987
  api = complete_proapi("/open/video/history", base_url)
3955
3988
  if isinstance(payload, str):
3956
3989
  payload = {"pick_code": payload}
3957
- return self.request(url=api, method="POST", data=payload, async_=async_, **request_kwargs)
3990
+ return self.request(url=api, params=payload, async_=async_, **request_kwargs)
3958
3991
 
3959
3992
  @overload
3960
- def fs_video_play(
3993
+ def fs_video_history_set(
3961
3994
  self,
3962
3995
  payload: str | dict,
3963
3996
  /,
@@ -3968,7 +4001,7 @@ class P115OpenClient(ClientRequestMixin):
3968
4001
  ) -> dict:
3969
4002
  ...
3970
4003
  @overload
3971
- def fs_video_play(
4004
+ def fs_video_history_set(
3972
4005
  self,
3973
4006
  payload: str | dict,
3974
4007
  /,
@@ -3978,7 +4011,7 @@ class P115OpenClient(ClientRequestMixin):
3978
4011
  **request_kwargs,
3979
4012
  ) -> Coroutine[Any, Any, dict]:
3980
4013
  ...
3981
- def fs_video_play(
4014
+ def fs_video_history_set(
3982
4015
  self,
3983
4016
  payload: str | dict,
3984
4017
  /,
@@ -3987,25 +4020,23 @@ class P115OpenClient(ClientRequestMixin):
3987
4020
  async_: Literal[False, True] = False,
3988
4021
  **request_kwargs,
3989
4022
  ) -> dict | Coroutine[Any, Any, dict]:
3990
- """获取视频在线播放地址(和视频文件相关数据)
4023
+ """记忆视频播放进度
3991
4024
 
3992
- GET https://proapi.115.com/open/video/play
3993
-
3994
- .. admonition:: Reference
4025
+ POST https://proapi.115.com/open/video/history
3995
4026
 
3996
- https://www.yuque.com/115yun/open/hqglxv3cedi3p9dz
4027
+ .. admonition:: Reference
3997
4028
 
3998
- .. hint::
3999
- 需切换音轨时,在请求返回的播放地址中增加请求参数 `&audio_track=${index}`,值就是接口响应中 `multitrack_list` 中某个成员的索引,从 0 开始计数
4029
+ https://www.yuque.com/115yun/open/bshagbxv1gzqglg4
4000
4030
 
4001
4031
  :payload:
4002
4032
  - pick_code: str 💡 文件提取码
4003
- - share_id: int | str = <default> 💡 共享 id,获取共享文件播放地址所需
4033
+ - time: int = <default> 💡 视频播放进度时长 (单位秒)
4034
+ - watch_end: int = <default> 💡 视频是否播放播放完毕 0:未完毕 1:完毕
4004
4035
  """
4005
- api = complete_proapi("/open/video/play", base_url)
4036
+ api = complete_proapi("/open/video/history", base_url)
4006
4037
  if isinstance(payload, str):
4007
4038
  payload = {"pick_code": payload}
4008
- return self.request(url=api, params=payload, async_=async_, **request_kwargs)
4039
+ return self.request(url=api, method="POST", data=payload, async_=async_, **request_kwargs)
4009
4040
 
4010
4041
  @overload
4011
4042
  def fs_video_push(
@@ -4048,13 +4079,15 @@ class P115OpenClient(ClientRequestMixin):
4048
4079
 
4049
4080
  :payload:
4050
4081
  - pick_code: str 💡 文件提取码
4051
- - op: str = "vip_push" 💡 提交视频加速转码方式:vip_push:根据;vip 等级加速 pay_push:枫叶加速
4082
+ - op: str = "vip_push" 💡 提交视频加速转码方式
4083
+
4084
+ - "vip_push": 根据;vip 等级加速
4085
+ - "pay_push": 枫叶加速
4052
4086
  """
4053
4087
  api = complete_proapi("/open/video/video_push", base_url)
4054
4088
  if isinstance(payload, str):
4055
- payload = {"pick_code": payload, "op": "vip_push"}
4056
- else:
4057
- payload.setdefault("op", "vip_push")
4089
+ payload = {"pick_code": payload}
4090
+ payload.setdefault("op", "vip_push")
4058
4091
  return self.request(url=api, method="POST", data=payload, async_=async_, **request_kwargs)
4059
4092
 
4060
4093
  @overload
@@ -5305,7 +5338,7 @@ class P115OpenClient(ClientRequestMixin):
5305
5338
  ) -> dict | Coroutine[Any, Any, dict]:
5306
5339
  """获取产品列表地址(即引导用户扫码购买 115 的 VIP 服务,以获取提成)
5307
5340
 
5308
- GET https://proapi.115.com/open/open/vip/qr_url
5341
+ GET https://proapi.115.com/open/vip/qr_url
5309
5342
 
5310
5343
  .. admonition:: Reference
5311
5344
 
@@ -5336,9 +5369,9 @@ class P115OpenClient(ClientRequestMixin):
5336
5369
  fs_move_open = fs_move
5337
5370
  fs_search_open = fs_search
5338
5371
  fs_star_set_open = fs_star_set
5372
+ fs_video_open = fs_video
5339
5373
  fs_video_history_open = fs_video_history
5340
5374
  fs_video_history_set_open = fs_video_history_set
5341
- fs_video_play_open = fs_video_play
5342
5375
  fs_video_push_open = fs_video_push
5343
5376
  fs_video_subtitle_open = fs_video_subtitle
5344
5377
  fs_update_open = fs_update
@@ -10960,6 +10993,8 @@ class P115Client(P115OpenClient):
10960
10993
  payload = {"fetch": "one", **payload}
10961
10994
  return self.request(url=api, params=payload, async_=async_, **request_kwargs)
10962
10995
 
10996
+ fs_video_history = fs_files_history
10997
+
10963
10998
  @overload
10964
10999
  def fs_files_history_set(
10965
11000
  self,
@@ -11002,6 +11037,7 @@ class P115Client(P115OpenClient):
11002
11037
  - definition: int = <default> 💡 视频清晰度
11003
11038
  - share_id: int | str = <default>
11004
11039
  - time: int = <default> 💡 播放时间点(用来向服务器同步播放进度)
11040
+ - watch_end: int = <default> 💡 视频是否播放播放完毕 0:未完毕 1:完毕
11005
11041
  - ...(其它未找全的参数)
11006
11042
  """
11007
11043
  api = complete_webapi("/files/history", base_url=base_url)
@@ -11011,6 +11047,8 @@ class P115Client(P115OpenClient):
11011
11047
  payload = {"op": "update", **payload}
11012
11048
  return self.request(url=api, method="POST", data=payload, async_=async_, **request_kwargs)
11013
11049
 
11050
+ fs_video_history_set = fs_files_history_set
11051
+
11014
11052
  @overload
11015
11053
  def fs_files_second_type(
11016
11054
  self,
@@ -13232,7 +13270,7 @@ class P115Client(P115OpenClient):
13232
13270
  GET https://proapi.115.com/android/music/musicplay
13233
13271
 
13234
13272
  .. note::
13235
- 即使文件格式不正确或者过大(超过 200MB),也可返回一些信息(包括 parent_id),但如果是目录则信息匮乏(但由此也可判定一个目录)
13273
+ 即使文件格式不正确或者过大(超过 200 MB),也可返回一些信息(包括 parent_id),但如果是目录则信息匮乏(但由此也可判定一个目录)
13236
13274
 
13237
13275
  :payload:
13238
13276
  - pickcode: str 💡 提取码
@@ -15374,11 +15412,21 @@ class P115Client(P115OpenClient):
15374
15412
 
15375
15413
  GET https://webapi.115.com/files/video
15376
15414
 
15415
+ .. caution::
15416
+ `local` 在有些视频上不起作用,无论如何,都相当于 `local=0`,可能是因为文件超过 200 MB
15417
+
15418
+ 但如果 `local=1` 有效,则返回仅可得到下载链接,key 为 "download_url"
15419
+
15377
15420
  .. important::
15378
15421
  仅这几种设备可用:`harmony`, `web`, `desktop`, **wechatmini**, **alipaymini**, **tv**
15379
15422
 
15380
15423
  但是如果要获取 m3u8 文件,则要提供 web 设备的 cookies,否则返回空数据
15381
15424
 
15425
+ .. note::
15426
+ 如果返回信息中有 "queue_url",则可用于查询转码状态
15427
+
15428
+ 如果视频从未被转码过,则会自动推送转码
15429
+
15382
15430
  :payload:
15383
15431
  - pickcode: str 💡 提取码
15384
15432
  - share_id: int | str = <default> 💡 分享 id
@@ -15650,6 +15698,57 @@ class P115Client(P115OpenClient):
15650
15698
  payload = {"pickcode": payload}
15651
15699
  return self.request(url=api, params=payload, async_=async_, **request_kwargs)
15652
15700
 
15701
+ @overload
15702
+ def fs_video_transcode(
15703
+ self,
15704
+ payload: dict | str,
15705
+ /,
15706
+ app: str = "web",
15707
+ base_url: bool | str | Callable[[], str] = False,
15708
+ method: str = "GET",
15709
+ *,
15710
+ async_: Literal[False] = False,
15711
+ **request_kwargs,
15712
+ ) -> dict:
15713
+ ...
15714
+ @overload
15715
+ def fs_video_transcode(
15716
+ self,
15717
+ payload: dict | str,
15718
+ /,
15719
+ app: str = "web",
15720
+ base_url: bool | str | Callable[[], str] = False,
15721
+ method: str = "GET",
15722
+ *,
15723
+ async_: Literal[True],
15724
+ **request_kwargs,
15725
+ ) -> Coroutine[Any, Any, dict]:
15726
+ ...
15727
+ def fs_video_transcode(
15728
+ self,
15729
+ payload: dict | str,
15730
+ /,
15731
+ app: str = "web",
15732
+ base_url: bool | str | Callable[[], str] = False,
15733
+ method: str = "GET",
15734
+ *,
15735
+ async_: Literal[False, True] = False,
15736
+ **request_kwargs,
15737
+ ) -> dict | Coroutine[Any, Any, dict]:
15738
+ """获取视频的转码进度
15739
+
15740
+ GET http://transcode.115.com/api/1.0/android/1.0/trans_code/check_transcode_job
15741
+
15742
+ :payload:
15743
+ - sha1: str
15744
+ - priority: int = 100 💡 优先级
15745
+ """
15746
+ api = complete_api(f"/api/1.0/{app}/1.0/trans_code/check_transcode_job", "transcode", base_url=base_url)
15747
+ if isinstance(payload, str):
15748
+ payload = {"sha1": payload}
15749
+ payload.setdefault("priority", 100)
15750
+ return self.request(url=api, method=method, params=payload, async_=async_, **request_kwargs)
15751
+
15653
15752
  ########## Life API ##########
15654
15753
 
15655
15754
  @overload
@@ -22921,13 +23020,22 @@ class P115Client(P115OpenClient):
22921
23020
  return self.request(url=api, method="POST", data=payload, async_=async_, **request_kwargs)
22922
23021
 
22923
23022
 
22924
- for name, method in P115Client.__dict__.items():
22925
- if not (callable(method) and method.__doc__):
22926
- continue
22927
- match = CRE_CLIENT_API_search(method.__doc__)
22928
- if match is not None:
22929
- CLIENT_API_MAP[match[1]] = "P115Client." + name
23023
+ with temp_globals():
23024
+ CRE_CLIENT_API_search: Final = re_compile(r"^ +((?:GET|POST|PUT|DELETE|PATCH) .*)", MULTILINE).search
23025
+ for name in dir(P115Client):
23026
+ method = getattr(P115Client, name)
23027
+ if not (callable(method) and method.__doc__):
23028
+ continue
23029
+ match = CRE_CLIENT_API_search(method.__doc__)
23030
+ if match is not None:
23031
+ api = match[1]
23032
+ name = "P115Client." + name
23033
+ CLIENT_METHOD_API_MAP[name] = api
23034
+ try:
23035
+ CLIENT_API_METHODS_MAP[api].append(name)
23036
+ except KeyError:
23037
+ CLIENT_API_METHODS_MAP[api] = [name]
23038
+
22930
23039
 
22931
23040
  # TODO: 提供一个可随时终止和暂停的上传功能,并且可以输出进度条和获取进度
22932
23041
  # TODO: 更新一下,p115client._upload,做更多的封装,至少让断点续传更易于使用
22933
-
@@ -5,8 +5,8 @@ from __future__ import annotations
5
5
 
6
6
  __author__ = "ChenyangGao <https://chenyanggao.github.io>"
7
7
  __all__ = [
8
- "AVAILABLE_APPS", "APP_TO_SSOENT", "SSOENT_TO_APP", "CLIENT_API_MAP",
9
- "CLASS_TO_TYPE", "SUFFIX_TO_TYPE", "errno",
8
+ "AVAILABLE_APPS", "APP_TO_SSOENT", "SSOENT_TO_APP", "CLIENT_METHOD_API_MAP",
9
+ "CLIENT_API_METHODS_MAP", "CLASS_TO_TYPE", "SUFFIX_TO_TYPE", "errno",
10
10
  ]
11
11
 
12
12
  from enum import IntEnum
@@ -69,8 +69,11 @@ SSOENT_TO_APP: Final[dict[str, str]] = {
69
69
  "S1": "harmony",
70
70
  }
71
71
 
72
- #: 所有已封装的 115 接口以及对应的方法名
73
- CLIENT_API_MAP: Final[dict[str, str]] = {}
72
+ #: 所有已封装的方法名和对应的 115 接口
73
+ CLIENT_METHOD_API_MAP: Final[dict[str, str]] = {}
74
+
75
+ #: 所有已封装的 115 接口和对应的方法名
76
+ CLIENT_API_METHODS_MAP: Final[dict[str, list[str]]] = {}
74
77
 
75
78
  #: 文件的 class 属性对应的所属类型的整数代码
76
79
  CLASS_TO_TYPE: Final[dict[str, int]] = {
@@ -7,12 +7,14 @@ __doc__ = "这个模块提供了一些和离线下载有关的函数"
7
7
 
8
8
  from asyncio import sleep as async_sleep
9
9
  from collections.abc import AsyncIterator, Callable, Iterable, Iterator
10
+ from errno import EBUSY
10
11
  from itertools import count
11
12
  from time import sleep, time
12
13
  from typing import overload, Literal
13
14
 
14
15
  from iterutils import run_gen_step_iter, with_iter_next, Yield, YieldFrom
15
16
  from p115client import check_response, P115Client, P115OpenClient
17
+ from p115client.exception import BusyOSError
16
18
 
17
19
 
18
20
  @overload
@@ -22,7 +24,8 @@ def offline_iter(
22
24
  page_start: int = 1,
23
25
  page_stop: int = -1,
24
26
  cooldown: float = 0,
25
- use_open_api: bool = False,
27
+ raise_for_update: bool = False,
28
+ use_open_api: bool = False,
26
29
  *,
27
30
  async_: Literal[False] = False,
28
31
  **request_kwargs,
@@ -35,6 +38,7 @@ def offline_iter(
35
38
  page_start: int = 1,
36
39
  page_stop: int = -1,
37
40
  cooldown: float = 0,
41
+ raise_for_update: bool = False,
38
42
  use_open_api: bool = False,
39
43
  *,
40
44
  async_: Literal[True],
@@ -47,6 +51,7 @@ def offline_iter(
47
51
  page_start: int = 1,
48
52
  page_stop: int = -1,
49
53
  cooldown: float = 0,
54
+ raise_for_update: bool = False,
50
55
  use_open_api: bool = False,
51
56
  *,
52
57
  async_: Literal[False, True] = False,
@@ -54,10 +59,18 @@ def offline_iter(
54
59
  ) -> Iterator[dict] | AsyncIterator[dict]:
55
60
  """遍历任务列表,获取任务信息
56
61
 
62
+ .. tip::
63
+ 在逐页拉取的间隔期间,任务列表可能发生变化,可能导致重复和遗漏:
64
+
65
+ 1. 新增任务,特别是状态为进行中
66
+ 2. 删除任务
67
+ 3. 曾经取得的进行中的任务,变为完成
68
+
57
69
  :param client: 115 客户端或 cookies
58
70
  :param page_start: 开始页数
59
71
  :param page_stop: 结束页数(不含),如果 <= 0,则不限
60
72
  :param cooldown: 接口调用冷却时间,单位:秒
73
+ :param raise_for_update: 当列表发生更新时,是否报错退出
61
74
  :param use_open_api: 是否使用 open api
62
75
  :param async_: 是否异步
63
76
  :param request_kwargs: 其它请求参数
@@ -79,21 +92,40 @@ def offline_iter(
79
92
  offline_list = client.offline_list_open
80
93
  else:
81
94
  offline_list = client.offline_list
82
- if cooldown > 0:
95
+ may_sleep = cooldown > 0
96
+ if may_sleep:
83
97
  do_sleep = async_sleep if async_ else sleep
84
- last_t: float = 0
98
+ last_t: float = 0
99
+ if raise_for_update:
100
+ count = -1
101
+ seen: set[str] = set()
102
+ add_info_hash = seen.add
85
103
  for page in pages:
86
- if last_t and (diff := last_t + cooldown - time()) > 0:
87
- yield do_sleep(diff)
88
- last_t = time()
104
+ if may_sleep:
105
+ if last_t and (diff := last_t + cooldown - time()) > 0:
106
+ yield do_sleep(diff)
107
+ last_t = time()
89
108
  resp = yield offline_list(page, async_=async_, **request_kwargs)
90
109
  check_response(resp)
91
110
  if use_open_api:
92
111
  resp = resp["data"]
112
+ if raise_for_update:
113
+ if count < 0:
114
+ count = resp["count"]
115
+ elif count != resp["count"]:
116
+ raise BusyOSError(EBUSY, f"detected count changes: {count} != {resp['count']}")
93
117
  tasks = resp["tasks"]
94
118
  if not tasks:
95
119
  break
96
- yield YieldFrom(resp["tasks"])
120
+ if raise_for_update:
121
+ for task in tasks:
122
+ info_hash = task["info_hash"]
123
+ if info_hash in seen:
124
+ raise BusyOSError(EBUSY, f"detected duplicate task: info_hash={info_hash!r}")
125
+ add_info_hash(info_hash)
126
+ yield Yield(task)
127
+ else:
128
+ yield YieldFrom(resp["tasks"])
97
129
  if len(tasks) < 30 or page >= resp["page_count"]:
98
130
  break
99
131
  return run_gen_step_iter(gen_step, async_=async_)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "p115client"
3
- version = "0.0.5.12"
3
+ version = "0.0.5.12.1"
4
4
  description = "Python 115 webdisk client."
5
5
  authors = ["ChenyangGao <wosiwujm@gmail.com>"]
6
6
  license = "MIT"
File without changes
File without changes