p115client 0.0.5.9.2__py3-none-any.whl → 0.0.5.10__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.
@@ -3,14 +3,13 @@
3
3
 
4
4
  __author__ = "ChenyangGao <https://chenyanggao.github.io>"
5
5
  __all__ = [
6
- "ID_TO_DIRNODE_CACHE", "P115ID", "unescape_115_charref", "posix_escape_name",
7
- "type_of_attr", "get_path_to_cid", "get_file_count", "get_ancestors",
6
+ "ID_TO_DIRNODE_CACHE", "DirNode", "get_path_to_cid", "get_file_count", "get_ancestors",
8
7
  "get_ancestors_to_cid", "get_id_to_path", "get_id_to_sha1", "get_id_to_pickcode",
9
8
  "iter_nodes_skim", "iter_stared_dirs_raw", "iter_stared_dirs", "ensure_attr_path",
10
9
  "ensure_attr_path_by_category_get", "iterdir_raw", "iterdir", "iterdir_limited",
11
10
  "iter_files_raw", "iter_files", "traverse_files", "iter_dirs", "iter_dupfiles",
12
- "iter_image_files", "share_extract_payload", "share_iterdir", "share_iter_files",
13
- "iter_selected_nodes", "iter_selected_nodes_by_pickcode", "iter_selected_nodes_using_category_get",
11
+ "iter_image_files", "share_iterdir", "share_iter_files", "iter_selected_nodes",
12
+ "iter_selected_nodes_by_pickcode", "iter_selected_nodes_using_category_get",
14
13
  "iter_selected_nodes_using_edit", "iter_selected_nodes_using_star_event",
15
14
  "iter_selected_dirs_using_star", "iter_files_with_dirname", "iter_files_with_path",
16
15
  "iter_files_with_path_by_export_dir", "iter_parents_3_level", "iter_dir_nodes",
@@ -26,22 +25,21 @@ from collections.abc import (
26
25
  from dataclasses import dataclass
27
26
  from errno import EIO, ENOENT, ENOTDIR
28
27
  from functools import partial
29
- from itertools import chain, count, cycle, islice, takewhile
28
+ from itertools import chain, cycle, islice
30
29
  from math import inf
31
30
  from operator import itemgetter
32
- from re import compile as re_compile
33
31
  from string import digits, hexdigits
34
32
  from threading import Lock
35
33
  from time import sleep, time
36
34
  from types import EllipsisType
37
- from typing import cast, overload, Any, Final, Literal, NamedTuple, TypedDict
35
+ from typing import cast, overload, Any, Final, Literal, NamedTuple
38
36
  from warnings import warn
39
37
  from weakref import WeakValueDictionary
40
38
 
41
39
  from asynctools import async_chain, async_filter, async_map, to_list
42
40
  from concurrenttools import run_as_thread, taskgroup_map, threadpool_map
43
41
  from iterutils import (
44
- as_gen_step, bfs_gen, chunked, async_foreach, ensure_aiter, foreach,
42
+ as_gen_step, bfs_gen, chunked, ensure_aiter, foreach,
45
43
  flatten, iter_unique, run_gen_step, run_gen_step_iter, through,
46
44
  async_through, with_iter_next, Yield, YieldFrom,
47
45
  )
@@ -51,18 +49,16 @@ from p115client import (
51
49
  check_response, normalize_attr, normalize_attr_simple,
52
50
  P115Client, P115OSError, P115Warning,
53
51
  )
54
- from p115client.const import CLASS_TO_TYPE, SUFFIX_TO_TYPE
55
- from p115client.type import P115DictAttrLike
52
+ from p115client.type import P115ID
56
53
  from posixpatht import joins, path_is_dir_form, splitext, splits
57
54
 
55
+ from .attr import type_of_attr
58
56
  from .edit import update_desc, update_star
59
57
  from .fs_files import is_timeouterror, iter_fs_files, iter_fs_files_threaded, iter_fs_files_asynchronized
60
58
  from .life import iter_life_behavior_once, life_show
59
+ from .util import posix_escape_name, share_extract_payload, unescape_115_charref
61
60
 
62
61
 
63
- CRE_SHARE_LINK_search1 = re_compile(r"(?:/s/|share\.115\.com/)(?P<share_code>[a-z0-9]+)\?password=(?:(?P<receive_code>[a-z0-9]{4}))?").search
64
- CRE_SHARE_LINK_search2 = re_compile(r"(?P<share_code>[a-z0-9]+)(?:-(?P<receive_code>[a-z0-9]{4}))?").search
65
- CRE_115_CHARREF_sub = re_compile("\\[\x02([0-9]+)\\]").sub
66
62
  WEBAPI_BASE_URLS = (
67
63
  "http://webapi.115.com",
68
64
  "https://webapi.115.com",
@@ -111,11 +107,6 @@ class OverviewAttr:
111
107
  ID_TO_DIRNODE_CACHE: Final[defaultdict[int, dict[int, tuple[str, int] | DirNode]]] = defaultdict(dict)
112
108
 
113
109
 
114
- class SharePayload(TypedDict):
115
- share_code: str
116
- receive_code: None | str
117
-
118
-
119
110
  def _overview_attr(info: Mapping, /) -> OverviewAttr:
120
111
  if "n" in info:
121
112
  is_dir = "fid" not in info
@@ -154,58 +145,6 @@ def _overview_attr(info: Mapping, /) -> OverviewAttr:
154
145
  return OverviewAttr(is_dir, id, pid, name, ctime, mtime)
155
146
 
156
147
 
157
- def posix_escape_name(name: str, /, repl: str = "|") -> str:
158
- """把文件名中的 "/" 转换为另一个字符(默认为 "|")
159
-
160
- :param name: 文件名
161
- :param repl: 替换为的目标字符
162
-
163
- :return: 替换后的名字
164
- """
165
- return name.replace("/", repl)
166
-
167
-
168
- def unescape_115_charref(s: str, /) -> str:
169
- """对 115 的字符引用进行解码
170
-
171
- :example:
172
-
173
- .. code:: python
174
-
175
- unescape_115_charref("[\x02128074]0号:优质资源") == "👊0号:优质资源"
176
- """
177
- return CRE_115_CHARREF_sub(lambda a: chr(int(a[1])), s)
178
-
179
-
180
- def type_of_attr(attr: Mapping, /) -> int:
181
- """推断文件信息所属类型(试验版,未必准确)
182
-
183
- :param attr: 文件信息
184
-
185
- :return: 返回类型代码
186
-
187
- - 0: 目录
188
- - 1: 文档
189
- - 2: 图片
190
- - 3: 音频
191
- - 4: 视频
192
- - 5: 压缩包
193
- - 6: 应用
194
- - 7: 书籍
195
- - 99: 其它文件
196
- """
197
- if attr.get("is_dir") or attr.get("is_directory"):
198
- return 0
199
- type: None | int
200
- if type := CLASS_TO_TYPE.get(attr.get("class", "")):
201
- return type
202
- if type := SUFFIX_TO_TYPE.get(splitext(attr["name"])[1].lower()):
203
- return type
204
- if attr.get("is_video") or "defination" in attr:
205
- return 4
206
- return 99
207
-
208
-
209
148
  @overload
210
149
  def get_path_to_cid(
211
150
  client: str | P115Client,
@@ -657,10 +596,6 @@ def get_ancestors_to_cid(
657
596
  return run_gen_step(gen_step, async_=async_)
658
597
 
659
598
 
660
- class P115ID(P115DictAttrLike, int):
661
-
662
- def __str__(self, /) -> str:
663
- return int.__repr__(self)
664
599
 
665
600
 
666
601
  # TODO: 使用 search 接口以在特定目录之下搜索某个名字,以便减少风控
@@ -908,6 +843,7 @@ def get_id_to_path(
908
843
  def get_id_to_pickcode(
909
844
  client: str | P115Client,
910
845
  pickcode: str,
846
+ app: str = "web",
911
847
  *,
912
848
  async_: Literal[False] = False,
913
849
  **request_kwargs,
@@ -917,6 +853,7 @@ def get_id_to_pickcode(
917
853
  def get_id_to_pickcode(
918
854
  client: str | P115Client,
919
855
  pickcode: str,
856
+ app: str = "web",
920
857
  *,
921
858
  async_: Literal[True],
922
859
  **request_kwargs,
@@ -925,21 +862,34 @@ def get_id_to_pickcode(
925
862
  def get_id_to_pickcode(
926
863
  client: str | P115Client,
927
864
  pickcode: str,
865
+ app: str = "web",
928
866
  *,
929
867
  async_: Literal[False, True] = False,
930
868
  **request_kwargs,
931
869
  ) -> P115ID | Coroutine[Any, Any, P115ID]:
870
+ """获取 pickcode 对应的 id
871
+
872
+ :param client: 115 客户端或 cookies
873
+ :param pickcode: 提取码
874
+ :param app: 使用某个 app (设备)的接口
875
+ :param async_: 是否异步
876
+ :param request_kwargs: 其它请求参数
877
+
878
+ :return: 文件或目录的 id
879
+ """
932
880
  if not 17 <= len(pickcode) <= 18 or not pickcode.isalnum():
933
881
  raise ValueError(f"bad pickcode: {pickcode!r}")
934
882
  if isinstance(client, str):
935
883
  client = P115Client(client, check_for_relogin=True)
936
884
  def gen_step():
937
- resp = yield client.download_url_web(pickcode, base_url=True, async_=async_, **request_kwargs)
938
- if file_id := resp.get("file_id"):
939
- msg_code = resp.get("msg_code", False)
940
- resp["is_dir"] = msg_code and msg_code != 50028
941
- return P115ID(file_id, resp, about="pickcode")
885
+ if app in ("", "web", "desktop", "harmony"):
886
+ fs_supervision: Callable = client.fs_supervision
887
+ else:
888
+ fs_supervision = partial(client.fs_supervision_app, app=app)
889
+ resp = yield fs_supervision(pickcode, async_=async_, **request_kwargs)
942
890
  check_response(resp)
891
+ data = resp["data"]
892
+ return P115ID(data["file_id"], data, about="pickcode")
943
893
  return run_gen_step(gen_step, async_=async_)
944
894
 
945
895
 
@@ -947,6 +897,7 @@ def get_id_to_pickcode(
947
897
  def get_id_to_sha1(
948
898
  client: str | P115Client,
949
899
  sha1: str,
900
+ app: str = "web",
950
901
  *,
951
902
  async_: Literal[False] = False,
952
903
  **request_kwargs,
@@ -956,6 +907,7 @@ def get_id_to_sha1(
956
907
  def get_id_to_sha1(
957
908
  client: str | P115Client,
958
909
  sha1: str,
910
+ app: str = "web",
959
911
  *,
960
912
  async_: Literal[True],
961
913
  **request_kwargs,
@@ -964,19 +916,40 @@ def get_id_to_sha1(
964
916
  def get_id_to_sha1(
965
917
  client: str | P115Client,
966
918
  sha1: str,
919
+ app: str = "web",
967
920
  *,
968
921
  async_: Literal[False, True] = False,
969
922
  **request_kwargs,
970
923
  ) -> P115ID | Coroutine[Any, Any, P115ID]:
924
+ """获取 sha1 对应的文件的 id
925
+
926
+ :param client: 115 客户端或 cookies
927
+ :param sha1: sha1 摘要值
928
+ :param app: 使用某个 app (设备)的接口
929
+ :param async_: 是否异步
930
+ :param request_kwargs: 其它请求参数
931
+
932
+ :return: 文件或目录的 id
933
+ """
971
934
  if len(sha1) != 40 or sha1.strip(hexdigits):
972
935
  raise ValueError(f"bad sha1: {sha1!r}")
973
936
  if isinstance(client, str):
974
937
  client = P115Client(client, check_for_relogin=True)
975
938
  def gen_step():
976
- resp = yield client.fs_shasearch(sha1, base_url=True, async_=async_, **request_kwargs)
977
- check_response(resp)
978
- resp["data"]["file_sha1"] = sha1.upper()
979
- return P115ID(resp["data"]["file_id"], resp["data"], about="sha1")
939
+ file_sha1 = sha1.upper()
940
+ if app in ("", "web", "desktop", "harmony"):
941
+ resp = yield client.fs_shasearch(sha1, async_=async_, **request_kwargs)
942
+ check_response(resp)
943
+ data = resp["data"]
944
+ else:
945
+ resp = yield client.fs_search_app(sha1, async_=async_, **request_kwargs)
946
+ check_response(resp)
947
+ for data in resp["data"]:
948
+ if data["sha1"] == file_sha1:
949
+ break
950
+ else:
951
+ raise FileNotFoundError(ENOENT, file_sha1)
952
+ return P115ID(data["file_id"], data, about="sha1", file_sha1=file_sha1)
980
953
  return run_gen_step(gen_step, async_=async_)
981
954
 
982
955
 
@@ -3135,24 +3108,6 @@ def iter_image_files(
3135
3108
  return run_gen_step_iter(gen_step, async_=async_)
3136
3109
 
3137
3110
 
3138
- def share_extract_payload(link: str, /) -> SharePayload:
3139
- """从链接中提取 share_code 和 receive_code
3140
-
3141
- .. hint::
3142
- `link` 支持 3 种形式(圆括号中的字符表示可有可无):
3143
-
3144
- 1. http(s)://115.com/s/{share_code}?password={receive_code}(#) 或 http(s)://share.115.com/{share_code}?password={receive_code}(#)
3145
- 2. (/){share_code}-{receive_code}(/)
3146
- 3. {share_code}
3147
- """
3148
- m = CRE_SHARE_LINK_search1(link)
3149
- if m is None:
3150
- m = CRE_SHARE_LINK_search2(link)
3151
- if m is None:
3152
- raise ValueError("not a valid 115 share link")
3153
- return cast(SharePayload, m.groupdict())
3154
-
3155
-
3156
3111
  @overload
3157
3112
  def share_iterdir(
3158
3113
  client: str | P115Client,
p115client/tool/life.py CHANGED
@@ -11,7 +11,7 @@ __doc__ = "这个模块提供了一些和 115 生活操作事件有关的函数"
11
11
  from asyncio import sleep as async_sleep
12
12
  from collections.abc import AsyncIterator, Container, Coroutine, Iterator
13
13
  from functools import partial
14
- from itertools import count, cycle
14
+ from itertools import cycle
15
15
  from time import time, sleep
16
16
  from typing import overload, Any, Final, Literal
17
17
 
p115client/tool/pool.py CHANGED
@@ -18,12 +18,14 @@ from itertools import cycle, repeat
18
18
  from math import inf, isinf
19
19
  from threading import Lock
20
20
  from time import time
21
- from typing import cast, Any
22
21
 
23
22
  from iterutils import run_gen_step
24
23
  from p115client import check_response, P115Client
24
+ from p115client.const import AVAILABLE_APP_IDS
25
25
  from p115client.exception import P115OSError, AuthenticationError, LoginError
26
26
 
27
+ from .util import get_status, is_timeouterror
28
+
27
29
 
28
30
  @total_ordering
29
31
  class ComparedWithID[T]:
@@ -51,35 +53,9 @@ class ComparedWithID[T]:
51
53
  return f"{type(self).__qualname__}({self.value!r})"
52
54
 
53
55
 
54
- def get_status(e: BaseException, /) -> None | int:
55
- status = (
56
- getattr(e, "status", None) or
57
- getattr(e, "code", None) or
58
- getattr(e, "status_code", None)
59
- )
60
- if status is None and hasattr(e, "response"):
61
- response = e.response
62
- status = (
63
- getattr(response, "status", None) or
64
- getattr(response, "code", None) or
65
- getattr(response, "status_code", None)
66
- )
67
- return status
68
-
69
-
70
- def is_timeouterror(exc: Exception) -> bool:
71
- exctype = type(exc)
72
- for exctype in exctype.mro():
73
- if exctype is Exception:
74
- break
75
- if "Timeout" in exctype.__name__:
76
- return True
77
- return False
78
-
79
-
80
56
  def generate_auth_factory(
81
57
  client: str | P115Client,
82
- app_ids: Iterable[int] = range(100195123, 100196659, 2),
58
+ app_ids: Iterable[int] = AVAILABLE_APP_IDS,
83
59
  **request_kwargs,
84
60
  ) -> Callable:
85
61
  """利用一个已登录设备的 cookies,产生若干开放应用的 access_token
@@ -262,7 +238,7 @@ def make_pool[T](
262
238
 
263
239
  def auth_pool(
264
240
  client: str | P115Client,
265
- app_ids: Iterable[int] = range(100195123, 100196659, 2),
241
+ app_ids: Iterable[int] = AVAILABLE_APP_IDS,
266
242
  heap: None | list[tuple[float, dict | ComparedWithID[dict]]] = None,
267
243
  cooldown_time: int | float = 1,
268
244
  live_time: int | float = 7000,
@@ -404,6 +380,7 @@ def call_wrap_with_pool(get_cert_headers: Callable, /, func: Callable) -> Callab
404
380
  return run_gen_step(gen_step, async_=async_)
405
381
  return update_wrapper(wrapper, func)
406
382
 
383
+
407
384
  # TODO: 需要完整的类型签名
408
385
  # TODO: 池子可以被导出,下次继续使用
409
386
  # TODO: 支持多个不同设备的 cookies 组成池,以及刷新(自己刷新自己,或者由另一个 cookies 辅助刷新)
@@ -126,6 +126,7 @@ def make_request(
126
126
  case _:
127
127
  raise ValueError(f"can't make request for {module!r}")
128
128
 
129
+
129
130
  # TODO: 基于 http.client 实现一个 request,并且支持连接池
130
131
  # TODO: 基于 https://asks.readthedocs.io/en/latest/ 实现一个 request
131
132
  # TODO: 基于 https://pypi.org/project/pycurl/ 实现一个 request
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env python3
2
+ # encoding: utf-8
3
+
4
+ __author__ = "ChenyangGao <https://chenyanggao.github.io>"
5
+ __all__ = [
6
+ "get_status", "is_timeouterror", "posix_escape_name", "reduce_image_url_layers",
7
+ "share_extract_payload", "unescape_115_charref",
8
+ ]
9
+ __doc__ = "这个模块提供了一些工具函数"
10
+
11
+ from re import compile as re_compile
12
+ from typing import cast, Final, TypedDict
13
+ from urllib.parse import urlsplit
14
+
15
+
16
+ CRE_115_CHARREF_sub: Final = re_compile("\\[\x02([0-9]+)\\]").sub
17
+ CRE_SHARE_LINK_search1 = re_compile(r"(?:/s/|share\.115\.com/)(?P<share_code>[a-z0-9]+)\?password=(?:(?P<receive_code>[a-z0-9]{4}))?").search
18
+ CRE_SHARE_LINK_search2 = re_compile(r"(?P<share_code>[a-z0-9]+)(?:-(?P<receive_code>[a-z0-9]{4}))?").search
19
+
20
+
21
+ class SharePayload(TypedDict):
22
+ share_code: str
23
+ receive_code: None | str
24
+
25
+
26
+ def get_status(e: BaseException, /) -> None | int:
27
+ """获取 HTTP 请求异常的状态码(如果有的话)
28
+ """
29
+ status = (
30
+ getattr(e, "status", None) or
31
+ getattr(e, "code", None) or
32
+ getattr(e, "status_code", None)
33
+ )
34
+ if status is None and hasattr(e, "response"):
35
+ response = e.response
36
+ status = (
37
+ getattr(response, "status", None) or
38
+ getattr(response, "code", None) or
39
+ getattr(response, "status_code", None)
40
+ )
41
+ return status
42
+
43
+
44
+ def is_timeouterror(exc: BaseException) -> bool:
45
+ """判断是不是超时异常
46
+ """
47
+ exctype = type(exc)
48
+ if issubclass(exctype, TimeoutError):
49
+ return True
50
+ for exctype in exctype.mro():
51
+ if "Timeout" in exctype.__name__:
52
+ return True
53
+ return False
54
+
55
+
56
+ def posix_escape_name(name: str, /, repl: str = "|") -> str:
57
+ """把文件名中的 "/" 转换为另一个字符(默认为 "|")
58
+
59
+ :param name: 文件名
60
+ :param repl: 替换为的目标字符
61
+
62
+ :return: 替换后的名字
63
+ """
64
+ return name.replace("/", repl)
65
+
66
+
67
+ def reduce_image_url_layers(url: str, /, size: str | int = "") -> str:
68
+ """从图片的缩略图链接中提取信息,以减少一次 302 访问
69
+ """
70
+ if not url.startswith(("http://thumb.115.com/", "https://thumb.115.com/")):
71
+ return url
72
+ urlp = urlsplit(url)
73
+ sha1, _, size0 = urlp.path.rsplit("/")[-1].partition("_")
74
+ if size == "":
75
+ size = size0 or "0"
76
+ return f"https://imgjump.115.com/?sha1={sha1}&{urlp.query}&size={size}"
77
+
78
+
79
+ def share_extract_payload(link: str, /) -> SharePayload:
80
+ """从链接中提取 share_code 和 receive_code
81
+
82
+ .. hint::
83
+ `link` 支持 3 种形式(圆括号中的字符表示可有可无):
84
+
85
+ 1. http(s)://115.com/s/{share_code}?password={receive_code}(#) 或 http(s)://share.115.com/{share_code}?password={receive_code}(#)
86
+ 2. (/){share_code}-{receive_code}(/)
87
+ 3. {share_code}
88
+ """
89
+ m = CRE_SHARE_LINK_search1(link)
90
+ if m is None:
91
+ m = CRE_SHARE_LINK_search2(link)
92
+ if m is None:
93
+ raise ValueError("not a valid 115 share link")
94
+ return cast(SharePayload, m.groupdict())
95
+
96
+
97
+ def unescape_115_charref(s: str, /) -> str:
98
+ """对 115 的字符引用进行解码
99
+
100
+ :example:
101
+
102
+ .. code:: python
103
+
104
+ unescape_115_charref("[\x02128074]0号:优质资源") == "👊0号:优质资源"
105
+ """
106
+ return CRE_115_CHARREF_sub(lambda a: chr(int(a[1])), s)
107
+