p115client 0.0.5.13.2__tar.gz → 0.0.5.14.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.13.2 → p115client-0.0.5.14.1}/PKG-INFO +3 -3
  2. {p115client-0.0.5.13.2 → p115client-0.0.5.14.1}/p115client/client.py +13 -7
  3. {p115client-0.0.5.13.2 → p115client-0.0.5.14.1}/p115client/tool/download.py +18 -9
  4. {p115client-0.0.5.13.2 → p115client-0.0.5.14.1}/p115client/tool/iterdir.py +269 -143
  5. {p115client-0.0.5.13.2 → p115client-0.0.5.14.1}/pyproject.toml +3 -3
  6. {p115client-0.0.5.13.2 → p115client-0.0.5.14.1}/LICENSE +0 -0
  7. {p115client-0.0.5.13.2 → p115client-0.0.5.14.1}/p115client/__init__.py +0 -0
  8. {p115client-0.0.5.13.2 → p115client-0.0.5.14.1}/p115client/_upload.py +0 -0
  9. {p115client-0.0.5.13.2 → p115client-0.0.5.14.1}/p115client/const.py +0 -0
  10. {p115client-0.0.5.13.2 → p115client-0.0.5.14.1}/p115client/exception.py +0 -0
  11. {p115client-0.0.5.13.2 → p115client-0.0.5.14.1}/p115client/py.typed +0 -0
  12. {p115client-0.0.5.13.2 → p115client-0.0.5.14.1}/p115client/tool/__init__.py +0 -0
  13. {p115client-0.0.5.13.2 → p115client-0.0.5.14.1}/p115client/tool/attr.py +0 -0
  14. {p115client-0.0.5.13.2 → p115client-0.0.5.14.1}/p115client/tool/auth.py +0 -0
  15. {p115client-0.0.5.13.2 → p115client-0.0.5.14.1}/p115client/tool/edit.py +0 -0
  16. {p115client-0.0.5.13.2 → p115client-0.0.5.14.1}/p115client/tool/export_dir.py +0 -0
  17. {p115client-0.0.5.13.2 → p115client-0.0.5.14.1}/p115client/tool/fs_files.py +0 -0
  18. {p115client-0.0.5.13.2 → p115client-0.0.5.14.1}/p115client/tool/history.py +0 -0
  19. {p115client-0.0.5.13.2 → p115client-0.0.5.14.1}/p115client/tool/life.py +0 -0
  20. {p115client-0.0.5.13.2 → p115client-0.0.5.14.1}/p115client/tool/offline.py +0 -0
  21. {p115client-0.0.5.13.2 → p115client-0.0.5.14.1}/p115client/tool/pool.py +0 -0
  22. {p115client-0.0.5.13.2 → p115client-0.0.5.14.1}/p115client/tool/request.py +0 -0
  23. {p115client-0.0.5.13.2 → p115client-0.0.5.14.1}/p115client/tool/upload.py +0 -0
  24. {p115client-0.0.5.13.2 → p115client-0.0.5.14.1}/p115client/tool/util.py +0 -0
  25. {p115client-0.0.5.13.2 → p115client-0.0.5.14.1}/p115client/tool/xys.py +0 -0
  26. {p115client-0.0.5.13.2 → p115client-0.0.5.14.1}/p115client/type.py +0 -0
  27. {p115client-0.0.5.13.2 → p115client-0.0.5.14.1}/readme.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: p115client
3
- Version: 0.0.5.13.2
3
+ Version: 0.0.5.14.1
4
4
  Summary: Python 115 webdisk client.
5
5
  Home-page: https://github.com/ChenyangGao/p115client
6
6
  License: MIT
@@ -29,7 +29,7 @@ Requires-Dist: iter_collect (>=0.0.5.1)
29
29
  Requires-Dist: multidict
30
30
  Requires-Dist: orjson
31
31
  Requires-Dist: p115cipher (>=0.0.3)
32
- Requires-Dist: p115pickcode (>=0.0.2)
32
+ Requires-Dist: p115pickcode (>=0.0.4)
33
33
  Requires-Dist: posixpatht (>=0.0.3)
34
34
  Requires-Dist: python-argtools (>=0.0.1)
35
35
  Requires-Dist: python-asynctools (>=0.1.3)
@@ -41,7 +41,7 @@ Requires-Dist: python-filewrap (>=0.2.8)
41
41
  Requires-Dist: python-hashtools (>=0.0.3.3)
42
42
  Requires-Dist: python-http_request (>=0.0.6)
43
43
  Requires-Dist: python-httpfile (>=0.0.5.2)
44
- Requires-Dist: python-iterutils (>=0.2.5)
44
+ Requires-Dist: python-iterutils (>=0.2.5.3)
45
45
  Requires-Dist: python-property (>=0.0.3)
46
46
  Requires-Dist: python-startfile (>=0.0.2)
47
47
  Requires-Dist: python-undefined (>=0.0.3)
@@ -59,7 +59,7 @@ from orjson import dumps, loads
59
59
  from p115cipher.fast import (
60
60
  rsa_encode, rsa_decode, ecdh_encode_token, ecdh_aes_encode, ecdh_aes_decode, make_upload_payload,
61
61
  )
62
- from p115pickcode import id_to_pickcode
62
+ from p115pickcode import get_stable_point
63
63
  from property import locked_cacheproperty
64
64
  from re import compile as re_compile
65
65
  from startfile import startfile, startfile_async # type: ignore
@@ -679,8 +679,6 @@ def normalize_attr_web[D: dict[str, Any]](
679
679
  attr["size"] = int(info.get("s") or 0)
680
680
  if "pc" in info:
681
681
  attr["pickcode"] = info["pc"]
682
- else:
683
- attr["pickcode"] = id_to_pickcode(attr["id"], is_dir=is_dir)
684
682
  if simple:
685
683
  if "c" in info:
686
684
  attr["is_collect"] = int(info["c"])
@@ -818,8 +816,6 @@ def normalize_attr_app[D: dict[str, Any]](
818
816
  attr["size"] = int(info.get("fs") or 0)
819
817
  if "pc" in info:
820
818
  attr["pickcode"] = info["pc"]
821
- else:
822
- attr["pickcode"] = id_to_pickcode(attr["id"], is_dir=is_dir)
823
819
  if simple:
824
820
  if "ic" in info:
825
821
  attr["is_collect"] = int(info["ic"])
@@ -956,8 +952,6 @@ def normalize_attr_app2[D: dict[str, Any]](
956
952
  attr["size"] = int(info.get("file_size") or 0)
957
953
  if "pick_code" in info:
958
954
  attr["pickcode"] = info["pick_code"]
959
- else:
960
- attr["pickcode"] = id_to_pickcode(attr["id"], is_dir=is_dir)
961
955
  if simple:
962
956
  if "is_collect" in info:
963
957
  attr["is_collect"] = int(info["is_collect"])
@@ -3127,6 +3121,18 @@ class P115OpenClient(ClientRequestMixin):
3127
3121
  resp = check_response(self.user_info_open())
3128
3122
  return int(resp["data"]["user_id"])
3129
3123
 
3124
+ @locked_cacheproperty
3125
+ def pickcode_stable_point(self, /) -> str:
3126
+ """获取 pickcode 的不动点
3127
+
3128
+ .. todo::
3129
+ 不动点可能和用户 id 有某种联系,但目前样本不足,难以推断,以后再尝试分析
3130
+ """
3131
+ resp = self.fs_files({"show_dir": 1, "limit": 1, "cid": 0})
3132
+ check_response(resp)
3133
+ info = resp["data"][0]
3134
+ return get_stable_point(normalize_attr(info)["pickcode"])
3135
+
3130
3136
  @overload
3131
3137
  def refresh_access_token(
3132
3138
  self,
@@ -94,14 +94,15 @@ def batch_get_url(
94
94
  """
95
95
  if isinstance(client, str):
96
96
  client = P115Client(client, check_for_relogin=True)
97
+ stable_point = client.pickcode_stable_point
97
98
  if headers := request_kwargs.get("headers"):
98
99
  request_kwargs["headers"] = dict(headers, **{"user-agent": user_agent})
99
100
  else:
100
101
  request_kwargs["headers"] = {"user-agent": user_agent}
101
102
  if isinstance(pickcode, (int, str)):
102
- pickcode = to_pickcode(pickcode)
103
+ pickcode = to_pickcode(pickcode, stable_point)
103
104
  elif not isinstance(pickcode, str):
104
- pickcode = ",".join(map(to_pickcode, pickcode))
105
+ pickcode = ",".join(to_pickcode(pc, stable_point) for pc in pickcode)
105
106
  if not isinstance(client, P115Client) or app == "open":
106
107
  get_download_url: Callable = client.download_url_info_open
107
108
  else:
@@ -183,6 +184,7 @@ def iter_url_batches(
183
184
  """
184
185
  if isinstance(client, str):
185
186
  client = P115Client(client, check_for_relogin=True)
187
+ stable_point = client.pickcode_stable_point
186
188
  if headers := request_kwargs.get("headers"):
187
189
  request_kwargs["headers"] = dict(headers, **{"user-agent": user_agent})
188
190
  else:
@@ -194,7 +196,7 @@ def iter_url_batches(
194
196
  if batch_size <= 0:
195
197
  batch_size = 1
196
198
  def gen_step():
197
- for pcs in batched(map(to_pickcode, pickcodes), batch_size):
199
+ for pcs in batched((to_pickcode(pc, stable_point) for pc in pickcodes), batch_size):
198
200
  resp = yield get_download_url(
199
201
  ",".join(pcs),
200
202
  async_=async_,
@@ -1208,6 +1210,7 @@ def iter_download_nodes(
1208
1210
  """
1209
1211
  if isinstance(client, str):
1210
1212
  client = P115Client(client, check_for_relogin=True)
1213
+ stable_point = client.pickcode_stable_point
1211
1214
  get_base_url = cycle(("http://proapi.115.com", "https://proapi.115.com")).__next__
1212
1215
  if async_:
1213
1216
  if max_workers is None or max_workers <= 0:
@@ -1221,6 +1224,7 @@ def iter_download_nodes(
1221
1224
  if id_to_dirnode is None:
1222
1225
  id_to_dirnode = ID_TO_DIRNODE_CACHE[client.user_id]
1223
1226
  file_skim = client.fs_file_skim
1227
+ need_yield = files and ensure_name
1224
1228
  def normalize_attrs(attrs: list[dict], /):
1225
1229
  if files:
1226
1230
  for i, info in enumerate(attrs):
@@ -1243,7 +1247,7 @@ def iter_download_nodes(
1243
1247
  for attr in attrs:
1244
1248
  id_to_dirnode[attr["id"]] = DirNode(attr["name"], attr["parent_id"])
1245
1249
  return attrs
1246
- if files and ensure_name:
1250
+ if need_yield:
1247
1251
  prepare = normalize_attrs
1248
1252
  @as_gen_step
1249
1253
  def normalize_attrs(attrs: list[dict], /):
@@ -1262,6 +1266,7 @@ def iter_download_nodes(
1262
1266
  attr["sha1"] = node["sha1"]
1263
1267
  attr["name"] = unescape_115_charref(node["file_name"])
1264
1268
  return attrs
1269
+ need_yield = need_yield and async_
1265
1270
  get_nodes = partial(
1266
1271
  method,
1267
1272
  async_=async_,
@@ -1269,7 +1274,7 @@ def iter_download_nodes(
1269
1274
  )
1270
1275
  if max_workers == 1:
1271
1276
  def gen_step(pickcode: int | str, /):
1272
- pickcode = to_pickcode(pickcode)
1277
+ pickcode = to_pickcode(pickcode, stable_point)
1273
1278
  for i in count(1):
1274
1279
  payload = {"pickcode": pickcode, "page": i}
1275
1280
  resp = yield get_nodes(payload)
@@ -1300,7 +1305,10 @@ def iter_download_nodes(
1300
1305
  put(e)
1301
1306
  return
1302
1307
  data = resp["data"]
1303
- put((yield normalize_attrs(data["list"])))
1308
+ attrs = normalize_attrs(data["list"])
1309
+ if need_yield:
1310
+ attrs = yield attrs
1311
+ put(attrs)
1304
1312
  if not data["has_next_page"]:
1305
1313
  max_page = page
1306
1314
  def gen_step(pickcode: int | str, /):
@@ -1316,7 +1324,7 @@ def iter_download_nodes(
1316
1324
  n = executor._max_workers
1317
1325
  submit = executor.submit
1318
1326
  shutdown = lambda: executor.shutdown(False, cancel_futures=True)
1319
- pickcode = to_pickcode(pickcode)
1327
+ pickcode = to_pickcode(pickcode, stable_point)
1320
1328
  try:
1321
1329
  sentinel = object()
1322
1330
  countdown: Callable
@@ -1441,6 +1449,7 @@ def iter_download_files(
1441
1449
  """
1442
1450
  if isinstance(client, str):
1443
1451
  client = P115Client(client, check_for_relogin=True)
1452
+ stable_point = client.pickcode_stable_point
1444
1453
  if id_to_dirnode is None:
1445
1454
  id_to_dirnode = ID_TO_DIRNODE_CACHE[client.user_id]
1446
1455
  elif id_to_dirnode is ...:
@@ -1507,7 +1516,7 @@ def iter_download_files(
1507
1516
  ))
1508
1517
  finally:
1509
1518
  ancestors_loaded = True
1510
- def gen_step(pickcode: str = to_pickcode(cid), /):
1519
+ def gen_step(pickcode: str, /):
1511
1520
  nonlocal ancestors_loaded
1512
1521
  if pickcode:
1513
1522
  if cid:
@@ -1585,7 +1594,7 @@ def iter_download_files(
1585
1594
  for pickcode in pickcodes:
1586
1595
  yield YieldFrom(run_gen_step_iter(gen_step(pickcode), async_))
1587
1596
  ancestors_loaded = False
1588
- return run_gen_step_iter(gen_step, async_)
1597
+ return run_gen_step_iter(gen_step(to_pickcode(cid, stable_point)), async_)
1589
1598
 
1590
1599
 
1591
1600
  @overload
@@ -14,11 +14,13 @@ __all__ = [
14
14
  "iter_nodes", "iter_nodes_skim", "iter_nodes_by_pickcode",
15
15
  "iter_nodes_using_update", "iter_nodes_using_info",
16
16
  "iter_nodes_using_star_event", "iter_dir_nodes_using_star",
17
- "iter_parents", "iter_dupfiles", "iter_image_files", "search_iter",
18
- "share_iterdir", "share_iter_files", "share_search_iter",
17
+ "iter_parents", "iter_files_shortcut", "iter_dupfiles", "iter_image_files",
18
+ "search_iter", "share_iterdir", "share_iter_files", "share_search_iter",
19
19
  ]
20
20
  __doc__ = "这个模块提供了一些和目录信息罗列有关的函数"
21
21
 
22
+ # TODO: 再实现 2 个方法,利用 iter_download_nodes,一个有 path,一个没有,可以把某个目录下的所有节点都搞出来,导出时,先导出目录节点,再导出文件节点,但它们是并发执行的,然后必有字段:id, parent_id, pickcode, name, is_dir, sha1 等
23
+
22
24
  # TODO: 路径表示法,应该支持 / 和 > 开头,而不仅仅是 / 开头
23
25
  # TODO: 对于路径,增加 top_id 和 relpath 字段,表示搜素目录的 id 和相对于搜索路径的相对路径
24
26
  # TODO: get_id* 这类方法,应该放在 attr.py,用来获取某个 id 对应的值(根本还是 get_attr)
@@ -2292,6 +2294,7 @@ def iter_files_with_path(
2292
2294
  raise ValueError("please set the non-zero value of suffix or type")
2293
2295
  if isinstance(client, str):
2294
2296
  client = P115Client(client, check_for_relogin=True)
2297
+ stable_point = client.pickcode_stable_point
2295
2298
  if isinstance(escape, bool):
2296
2299
  if escape:
2297
2300
  from posixpatht import escape
@@ -2303,36 +2306,6 @@ def iter_files_with_path(
2303
2306
  elif id_to_dirnode is ...:
2304
2307
  id_to_dirnode = {}
2305
2308
  path_already = False
2306
- _path_already: None | bool = None if path_already else False
2307
- if not path_already:
2308
- from .download import iter_download_nodes
2309
- def set_path_already(*_):
2310
- nonlocal _path_already
2311
- _path_already = True
2312
- @as_gen_step
2313
- def fetch_dirs(id: int | str, /):
2314
- if id:
2315
- yield through(iter_download_nodes(
2316
- client,
2317
- to_pickcode(id),
2318
- files=False,
2319
- id_to_dirnode=id_to_dirnode,
2320
- max_workers=None,
2321
- async_=async_,
2322
- **request_kwargs,
2323
- ))
2324
- else:
2325
- with with_iter_next(iterdir(
2326
- client,
2327
- ensure_file=False,
2328
- id_to_dirnode=id_to_dirnode,
2329
- app=app,
2330
- async_=async_,
2331
- **request_kwargs,
2332
- )) as get_next:
2333
- while True:
2334
- attr = yield get_next()
2335
- yield fetch_dirs(attr["pickcode"])
2336
2309
  if with_ancestors:
2337
2310
  id_to_ancestors: dict[int, list[dict]] = {}
2338
2311
  def get_ancestors(id: int, attr: dict | tuple[str, int] | DirNode, /) -> list[dict]:
@@ -2374,17 +2347,8 @@ def iter_files_with_path(
2374
2347
  pass
2375
2348
  return attr
2376
2349
  cid = to_id(cid)
2377
- def gen_step():
2378
- nonlocal _path_already
2379
- cache: list[dict] = []
2380
- add_to_cache = cache.append
2381
- if not path_already:
2382
- if async_:
2383
- task: Any = create_task(fetch_dirs(cid))
2384
- else:
2385
- task = run_as_thread(fetch_dirs, cid)
2386
- task.add_done_callback(set_path_already)
2387
- with with_iter_next(iter_files(
2350
+ if path_already:
2351
+ return do_map(update_path, iter_files(
2388
2352
  client,
2389
2353
  cid,
2390
2354
  page_size=page_size,
@@ -2401,30 +2365,88 @@ def iter_files_with_path(
2401
2365
  cooldown=cooldown,
2402
2366
  async_=async_, # type: ignore
2403
2367
  **request_kwargs,
2404
- )) as get_next:
2405
- while True:
2406
- attr = yield get_next()
2407
- if _path_already is None:
2408
- yield Yield(update_path(attr))
2409
- elif _path_already:
2410
- if async_:
2411
- yield task
2368
+ ))
2369
+ else:
2370
+ _path_already: None | bool = None if path_already else False
2371
+ from .download import iter_download_nodes
2372
+ def set_path_already(*_):
2373
+ nonlocal _path_already
2374
+ _path_already = True
2375
+ @as_gen_step
2376
+ def fetch_dirs(id: int | str, /):
2377
+ if id:
2378
+ yield through(iter_download_nodes(
2379
+ client,
2380
+ to_pickcode(id, stable_point),
2381
+ files=False,
2382
+ id_to_dirnode=id_to_dirnode,
2383
+ max_workers=None,
2384
+ async_=async_,
2385
+ **request_kwargs,
2386
+ ))
2387
+ else:
2388
+ with with_iter_next(iterdir(
2389
+ client,
2390
+ ensure_file=False,
2391
+ id_to_dirnode=id_to_dirnode,
2392
+ app=app,
2393
+ async_=async_,
2394
+ **request_kwargs,
2395
+ )) as get_next:
2396
+ while True:
2397
+ attr = yield get_next()
2398
+ yield fetch_dirs(attr["pickcode"])
2399
+ def gen_step():
2400
+ nonlocal _path_already
2401
+ cache: list[dict] = []
2402
+ add_to_cache = cache.append
2403
+ if not path_already:
2404
+ if async_:
2405
+ task: Any = create_task(fetch_dirs(cid))
2406
+ else:
2407
+ task = run_as_thread(fetch_dirs, cid)
2408
+ task.add_done_callback(set_path_already)
2409
+ with with_iter_next(iter_files(
2410
+ client,
2411
+ cid,
2412
+ page_size=page_size,
2413
+ suffix=suffix,
2414
+ type=type,
2415
+ order=order,
2416
+ asc=asc,
2417
+ cur=cur,
2418
+ normalize_attr=normalize_attr,
2419
+ id_to_dirnode=id_to_dirnode,
2420
+ raise_for_changed_count=raise_for_changed_count,
2421
+ max_workers=max_workers,
2422
+ app=app,
2423
+ cooldown=cooldown,
2424
+ async_=async_, # type: ignore
2425
+ **request_kwargs,
2426
+ )) as get_next:
2427
+ while True:
2428
+ attr = yield get_next()
2429
+ if _path_already is None:
2430
+ yield Yield(update_path(attr))
2431
+ elif _path_already:
2432
+ if async_:
2433
+ yield task
2434
+ else:
2435
+ task.result()
2436
+ if cache:
2437
+ yield YieldFrom(map(update_path, cache))
2438
+ cache.clear()
2439
+ yield Yield(update_path(attr))
2440
+ _path_already = None
2412
2441
  else:
2413
- task.result()
2414
- if cache:
2415
- yield YieldFrom(map(update_path, cache))
2416
- cache.clear()
2417
- yield Yield(update_path(attr))
2418
- _path_already = None
2442
+ add_to_cache(attr)
2443
+ if cache:
2444
+ if async_:
2445
+ yield task
2419
2446
  else:
2420
- add_to_cache(attr)
2421
- if cache:
2422
- if async_:
2423
- yield task
2424
- else:
2425
- task.result()
2426
- yield YieldFrom(map(update_path, cache))
2427
- return run_gen_step_iter(gen_step, async_)
2447
+ task.result()
2448
+ yield YieldFrom(map(update_path, cache))
2449
+ return run_gen_step_iter(gen_step, async_)
2428
2450
 
2429
2451
 
2430
2452
  @overload
@@ -2494,6 +2516,7 @@ def iter_files_with_path_skim(
2494
2516
  from .download import iter_download_nodes
2495
2517
  if isinstance(client, str):
2496
2518
  client = P115Client(client, check_for_relogin=True)
2519
+ stable_point = client.pickcode_stable_point
2497
2520
  if isinstance(escape, bool):
2498
2521
  if escape:
2499
2522
  from posixpatht import escape
@@ -2505,45 +2528,6 @@ def iter_files_with_path_skim(
2505
2528
  elif id_to_dirnode is ...:
2506
2529
  id_to_dirnode = {}
2507
2530
  path_already = False
2508
- _path_already: None | bool = None if path_already else False
2509
- if not path_already:
2510
- def set_path_already(*_):
2511
- nonlocal _path_already
2512
- _path_already = True
2513
- @as_gen_step
2514
- def fetch_dirs(id: int | str, /):
2515
- if id:
2516
- if cid:
2517
- do_next: Callable = anext if async_ else next
2518
- yield do_next(_iter_fs_files(
2519
- client,
2520
- to_id(id),
2521
- page_size=1,
2522
- id_to_dirnode=id_to_dirnode,
2523
- async_=async_,
2524
- **request_kwargs,
2525
- ))
2526
- yield through(iter_download_nodes(
2527
- client,
2528
- to_pickcode(id),
2529
- files=False,
2530
- id_to_dirnode=id_to_dirnode,
2531
- max_workers=max_workers,
2532
- async_=async_,
2533
- **request_kwargs,
2534
- ))
2535
- else:
2536
- with with_iter_next(iterdir(
2537
- client,
2538
- ensure_file=False,
2539
- id_to_dirnode=id_to_dirnode,
2540
- app=app,
2541
- async_=async_,
2542
- **request_kwargs,
2543
- )) as get_next:
2544
- while True:
2545
- attr = yield get_next()
2546
- yield fetch_dirs(attr["pickcode"])
2547
2531
  if with_ancestors:
2548
2532
  id_to_ancestors: dict[int, list[dict]] = {}
2549
2533
  def get_ancestors(id: int, attr: dict | tuple[str, int] | DirNode, /) -> list[dict]:
@@ -2585,17 +2569,8 @@ def iter_files_with_path_skim(
2585
2569
  pass
2586
2570
  return attr
2587
2571
  cid = to_id(cid)
2588
- def gen_step():
2589
- nonlocal _path_already
2590
- cache: list[dict] = []
2591
- add_to_cache = cache.append
2592
- if not path_already:
2593
- if async_:
2594
- task: Any = create_task(fetch_dirs(cid))
2595
- else:
2596
- task = run_as_thread(fetch_dirs, cid)
2597
- task.add_done_callback(set_path_already)
2598
- with with_iter_next(iter_download_nodes(
2572
+ if path_already:
2573
+ return do_map(update_path, iter_download_nodes(
2599
2574
  client,
2600
2575
  cid,
2601
2576
  files=True,
@@ -2604,30 +2579,89 @@ def iter_files_with_path_skim(
2604
2579
  app=app,
2605
2580
  async_=async_,
2606
2581
  **request_kwargs,
2607
- )) as get_next:
2608
- while True:
2609
- attr = yield get_next()
2610
- if _path_already is None:
2611
- yield Yield(update_path(attr))
2612
- elif _path_already:
2613
- if async_:
2614
- yield task
2582
+ ))
2583
+ else:
2584
+ _path_already: None | bool = None if path_already else False
2585
+ def set_path_already(*_):
2586
+ nonlocal _path_already
2587
+ _path_already = True
2588
+ @as_gen_step
2589
+ def fetch_dirs(id: int | str, /):
2590
+ if id:
2591
+ if cid:
2592
+ do_next: Callable = anext if async_ else next
2593
+ yield do_next(_iter_fs_files(
2594
+ client,
2595
+ to_id(id),
2596
+ page_size=1,
2597
+ id_to_dirnode=id_to_dirnode,
2598
+ async_=async_,
2599
+ **request_kwargs,
2600
+ ))
2601
+ yield through(iter_download_nodes(
2602
+ client,
2603
+ to_pickcode(id, stable_point),
2604
+ files=False,
2605
+ id_to_dirnode=id_to_dirnode,
2606
+ max_workers=max_workers,
2607
+ async_=async_,
2608
+ **request_kwargs,
2609
+ ))
2610
+ else:
2611
+ with with_iter_next(iterdir(
2612
+ client,
2613
+ ensure_file=False,
2614
+ id_to_dirnode=id_to_dirnode,
2615
+ app=app,
2616
+ async_=async_,
2617
+ **request_kwargs,
2618
+ )) as get_next:
2619
+ while True:
2620
+ attr = yield get_next()
2621
+ yield fetch_dirs(attr["pickcode"])
2622
+ def gen_step():
2623
+ nonlocal _path_already
2624
+ cache: list[dict] = []
2625
+ add_to_cache = cache.append
2626
+ if not path_already:
2627
+ if async_:
2628
+ task: Any = create_task(fetch_dirs(cid))
2629
+ else:
2630
+ task = run_as_thread(fetch_dirs, cid)
2631
+ task.add_done_callback(set_path_already)
2632
+ with with_iter_next(iter_download_nodes(
2633
+ client,
2634
+ cid,
2635
+ files=True,
2636
+ ensure_name=True,
2637
+ max_workers=max_workers,
2638
+ app=app,
2639
+ async_=async_,
2640
+ **request_kwargs,
2641
+ )) as get_next:
2642
+ while True:
2643
+ attr = yield get_next()
2644
+ if _path_already is None:
2645
+ yield Yield(update_path(attr))
2646
+ elif _path_already:
2647
+ if async_:
2648
+ yield task
2649
+ else:
2650
+ task.result()
2651
+ if cache:
2652
+ yield YieldFrom(map(update_path, cache))
2653
+ cache.clear()
2654
+ yield Yield(update_path(attr))
2655
+ _path_already = None
2615
2656
  else:
2616
- task.result()
2617
- if cache:
2618
- yield YieldFrom(map(update_path, cache))
2619
- cache.clear()
2620
- yield Yield(update_path(attr))
2621
- _path_already = None
2657
+ add_to_cache(attr)
2658
+ if cache:
2659
+ if async_:
2660
+ yield task
2622
2661
  else:
2623
- add_to_cache(attr)
2624
- if cache:
2625
- if async_:
2626
- yield task
2627
- else:
2628
- task.result()
2629
- yield YieldFrom(map(update_path, cache))
2630
- return run_gen_step_iter(gen_step, async_)
2662
+ task.result()
2663
+ yield YieldFrom(map(update_path, cache))
2664
+ return run_gen_step_iter(gen_step, async_)
2631
2665
 
2632
2666
 
2633
2667
  @overload
@@ -2838,6 +2872,7 @@ def iter_nodes_by_pickcode(
2838
2872
  """
2839
2873
  if isinstance(client, str):
2840
2874
  client = P115Client(client, check_for_relogin=True)
2875
+ stable_point = client.pickcode_stable_point
2841
2876
  if id_to_dirnode is None:
2842
2877
  id_to_dirnode = ID_TO_DIRNODE_CACHE[client.user_id]
2843
2878
  methods: list[Callable] = []
@@ -2875,7 +2910,7 @@ def iter_nodes_by_pickcode(
2875
2910
  project,
2876
2911
  conmap(
2877
2912
  get_response,
2878
- map(to_pickcode, pickcodes),
2913
+ (to_pickcode(pc, stable_point) for pc in pickcodes),
2879
2914
  max_workers=max_workers,
2880
2915
  kwargs=request_kwargs,
2881
2916
  async_=async_,
@@ -3426,6 +3461,91 @@ def iter_parents(
3426
3461
  ))
3427
3462
 
3428
3463
 
3464
+ @overload
3465
+ def iter_files_shortcut(
3466
+ client: str | P115Client,
3467
+ cid: int | str = 0,
3468
+ id_to_dirnode: None | EllipsisType | MutableMapping[int, tuple[str, int] | DirNode] = None,
3469
+ max_workers: None | int = None,
3470
+ is_skim: bool = True,
3471
+ with_path: bool = False,
3472
+ app: str = "android",
3473
+ *,
3474
+ async_: Literal[False] = False,
3475
+ **request_kwargs,
3476
+ ) -> Iterator[dict]:
3477
+ ...
3478
+ @overload
3479
+ def iter_files_shortcut(
3480
+ client: str | P115Client,
3481
+ cid: int | str = 0,
3482
+ id_to_dirnode: None | EllipsisType | MutableMapping[int, tuple[str, int] | DirNode] = None,
3483
+ max_workers: None | int = None,
3484
+ is_skim: bool = True,
3485
+ with_path: bool = False,
3486
+ app: str = "android",
3487
+ *,
3488
+ async_: Literal[True],
3489
+ **request_kwargs,
3490
+ ) -> AsyncIterator[dict]:
3491
+ ...
3492
+ def iter_files_shortcut(
3493
+ client: str | P115Client,
3494
+ cid: int | str = 0,
3495
+ id_to_dirnode: None | EllipsisType | MutableMapping[int, tuple[str, int] | DirNode] = None,
3496
+ max_workers: None | int = None,
3497
+ is_skim: bool = True,
3498
+ with_path: bool = False,
3499
+ app: str = "android",
3500
+ *,
3501
+ async_: Literal[False, True] = False,
3502
+ **request_kwargs,
3503
+ ) -> Iterator[dict] | AsyncIterator[dict]:
3504
+ """遍历目录树,获取(仅文件而非目录)文件信息(整合了多个函数的入口)
3505
+
3506
+ .. node::
3507
+ `is_skim` 和 `with_path` 的不同取值组合,会决定采用不同的函数:
3508
+
3509
+ 1. `iter_download_nodes`: is_skim=True and with_path=False
3510
+ 2. `iter_files_with_path_skim`: is_skim=True and with_path=True
3511
+ 3. `iter_files`: is_skim=False and with_path=False
3512
+ 4. `iter_files_with_path`: is_skim=False and with_path=True
3513
+
3514
+ :param client: 115 客户端或 cookies
3515
+ :param cid: 待被遍历的目录 id 或 pickcode
3516
+ :param id_to_dirnode: 字典,保存 id 到对应文件的 `DirNode(name, parent_id)` 命名元组的字典
3517
+ :param max_workers: 最大并发数,如果为 None 或 <= 0,则自动确定
3518
+ :param is_skim: 是否拉取简要信息
3519
+ :param with_path: 是否需要 "path" 和 "ancestors" 字段
3520
+ :param app: 使用指定 app(设备)的接口
3521
+ :param async_: 是否异步
3522
+ :param request_kwargs: 其它请求参数
3523
+
3524
+ :return: 迭代器,产生文件信息
3525
+ """
3526
+ if with_path:
3527
+ request_kwargs.setdefault("with_ancestors", True)
3528
+ if is_skim:
3529
+ method: Callable = iter_files_with_path_skim
3530
+ else:
3531
+ method = iter_files_with_path
3532
+ elif is_skim:
3533
+ request_kwargs.update(files=True, ensure_name=True)
3534
+ from .download import iter_download_nodes as method
3535
+ else:
3536
+ request_kwargs.setdefault("cooldown", 0.5)
3537
+ method = iter_files
3538
+ return method(
3539
+ client,
3540
+ cid,
3541
+ id_to_dirnode=id_to_dirnode,
3542
+ max_workers=max_workers,
3543
+ app=app,
3544
+ async_=async_,
3545
+ **request_kwargs,
3546
+ )
3547
+
3548
+
3429
3549
  @overload
3430
3550
  def iter_dupfiles[K](
3431
3551
  client: str | P115Client,
@@ -3434,6 +3554,8 @@ def iter_dupfiles[K](
3434
3554
  keep_first: None | bool | Callable[[dict], SupportsLT] = None,
3435
3555
  id_to_dirnode: None | EllipsisType | MutableMapping[int, tuple[str, int] | DirNode] = None,
3436
3556
  max_workers: None | int = None,
3557
+ is_skim: bool = True,
3558
+ with_path: bool = False,
3437
3559
  app: str = "android",
3438
3560
  *,
3439
3561
  async_: Literal[False] = False,
@@ -3448,6 +3570,8 @@ def iter_dupfiles[K](
3448
3570
  keep_first: None | bool | Callable[[dict], SupportsLT] = None,
3449
3571
  id_to_dirnode: None | EllipsisType | MutableMapping[int, tuple[str, int] | DirNode] = None,
3450
3572
  max_workers: None | int = None,
3573
+ is_skim: bool = True,
3574
+ with_path: bool = False,
3451
3575
  app: str = "android",
3452
3576
  *,
3453
3577
  async_: Literal[True],
@@ -3461,6 +3585,8 @@ def iter_dupfiles[K](
3461
3585
  keep_first: None | bool | Callable[[dict], SupportsLT] = None,
3462
3586
  id_to_dirnode: None | EllipsisType | MutableMapping[int, tuple[str, int] | DirNode] = None,
3463
3587
  max_workers: None | int = None,
3588
+ is_skim: bool = True,
3589
+ with_path: bool = False,
3464
3590
  app: str = "android",
3465
3591
  *,
3466
3592
  async_: Literal[False, True] = False,
@@ -3480,22 +3606,22 @@ def iter_dupfiles[K](
3480
3606
 
3481
3607
  :param id_to_dirnode: 字典,保存 id 到对应文件的 `DirNode(name, parent_id)` 命名元组的字典
3482
3608
  :param max_workers: 最大并发数,如果为 None 或 <= 0,则自动确定
3609
+ :param is_skim: 是否拉取简要信息
3610
+ :param with_path: 是否需要 "path" 和 "ancestors" 字段
3483
3611
  :param app: 使用指定 app(设备)的接口
3484
- :param cooldown: 冷却时间,大于 0,则使用此时间间隔执行并发
3485
3612
  :param async_: 是否异步
3486
3613
  :param request_kwargs: 其它请求参数
3487
3614
 
3488
3615
  :return: 迭代器,返回 key 和 重复文件信息 的元组
3489
3616
  """
3490
- from .download import iter_download_nodes
3491
3617
  return iter_keyed_dups(
3492
- iter_download_nodes(
3618
+ iter_files_shortcut(
3493
3619
  client,
3494
3620
  cid,
3495
- files=True,
3496
- ensure_name=True,
3497
3621
  id_to_dirnode=id_to_dirnode,
3498
3622
  max_workers=max_workers,
3623
+ is_skim=is_skim,
3624
+ with_path=with_path,
3499
3625
  app=app,
3500
3626
  async_=async_, # type: ignore
3501
3627
  **request_kwargs,
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "p115client"
3
- version = "0.0.5.13.2"
3
+ version = "0.0.5.14.1"
4
4
  description = "Python 115 webdisk client."
5
5
  authors = ["ChenyangGao <wosiwujm@gmail.com>"]
6
6
  license = "MIT"
@@ -37,7 +37,7 @@ iter_collect = ">=0.0.5.1"
37
37
  multidict = "*"
38
38
  orjson = "*"
39
39
  p115cipher = ">=0.0.3"
40
- p115pickcode = ">=0.0.2"
40
+ p115pickcode = ">=0.0.4"
41
41
  posixpatht = ">=0.0.3"
42
42
  python-argtools = ">=0.0.1"
43
43
  python-asynctools = ">=0.1.3"
@@ -49,7 +49,7 @@ python-filewrap = ">=0.2.8"
49
49
  python-hashtools = ">=0.0.3.3"
50
50
  python-httpfile = ">=0.0.5.2"
51
51
  python-http_request = ">=0.0.6"
52
- python-iterutils = ">=0.2.5"
52
+ python-iterutils = ">=0.2.5.3"
53
53
  python-property = ">=0.0.3"
54
54
  python-startfile = ">=0.0.2"
55
55
  python-undefined = ">=0.0.3"
File without changes