p115client 0.0.5.10.8__py3-none-any.whl → 0.0.5.11__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
@@ -181,7 +181,7 @@ def complete_proapi(
181
181
  ) -> str:
182
182
  if path and not path.startswith("/"):
183
183
  path = "/" + path
184
- if app == "open":
184
+ if app in ("aps", "desktop", "open", "web"):
185
185
  app = "android"
186
186
  if app and not app.startswith("/"):
187
187
  app = "/" + app
@@ -266,7 +266,7 @@ def get_default_request():
266
266
  return _httpx_request
267
267
 
268
268
 
269
- def parse_upload_init_response(resp, content: bytes, /) -> dict:
269
+ def parse_upload_init_response(_, content: bytes, /) -> dict:
270
270
  data = ecdh_aes_decode(content, decompress=True)
271
271
  if not isinstance(data, (bytes, bytearray, memoryview)):
272
272
  data = memoryview(data)
@@ -542,6 +542,36 @@ def check_response(resp: dict | Awaitable[dict], /) -> dict | Coroutine[Any, Any
542
542
  # {"state": 0, "errno": 40140126, "error": "access_token 校验失败(防篡改)"}
543
543
  case 40140126:
544
544
  raise OperationalError(errno.EINVAL, resp)
545
+ # {"state": 0, "errno": 40140127, "error": "response_type 错误"}
546
+ case 40140127:
547
+ raise OperationalError(errno.EINVAL, resp)
548
+ # {"state": 0, "errno": 40140128, "error": "redirect_uri 缺少协议"}
549
+ case 40140128:
550
+ raise OperationalError(errno.EINVAL, resp)
551
+ # {"state": 0, "errno": 40140129, "error": "redirect_uri 缺少域名"}
552
+ case 40140129:
553
+ raise OperationalError(errno.EINVAL, resp)
554
+ # {"state": 0, "errno": 40140130, "error": "没有配置重定向域名"}
555
+ case 40140130:
556
+ raise OperationalError(errno.EINVAL, resp)
557
+ # {"state": 0, "errno": 40140131, "error": "redirect_uri 非法域名"}
558
+ case 40140131:
559
+ raise OperationalError(errno.EINVAL, resp)
560
+ # {"state": 0, "errno": 40140132, "error": "grant_type 错误"}
561
+ case 40140132:
562
+ raise OperationalError(errno.EINVAL, resp)
563
+ # {"state": 0, "errno": 40140133, "error": "client_secret 验证失败"}
564
+ case 40140133:
565
+ raise OperationalError(errno.EINVAL, resp)
566
+ # {"state": 0, "errno": 40140134, "error": "授权码 code 验证失败"}
567
+ case 40140134:
568
+ raise OperationalError(errno.EINVAL, resp)
569
+ # {"state": 0, "errno": 40140135, "error": "client_id 验证失败"}
570
+ case 40140135:
571
+ raise OperationalError(errno.EINVAL, resp)
572
+ # {"state": 0, "errno": 40140136, "error": "redirect_uri 验证失败(防MITM)"}
573
+ case 40140136:
574
+ raise OperationalError(errno.EINVAL, resp)
545
575
  elif "msg_code" in resp:
546
576
  match resp["msg_code"]:
547
577
  case 50028:
@@ -616,9 +646,6 @@ def normalize_attr_web(
616
646
  if "te" in info:
617
647
  attr["mtime"] = int(info["te"])
618
648
  else:
619
- attr["labels"] = info["fl"]
620
- if "score" in info:
621
- attr["score"] = int(info.get("score") or 0)
622
649
  attr["ico"] = info.get("ico", "folder" if is_directory else "")
623
650
  if "te" in info:
624
651
  attr["mtime"] = attr["user_utime"] = int(info["te"])
@@ -651,12 +678,14 @@ def normalize_attr_web(
651
678
  ("c", "violated"),
652
679
  ("c", "is_collect"),
653
680
  ("sh", "is_share"),
681
+ ("score", "score"),
654
682
  #("d", "has_desc"),
655
683
  #("p", "has_pass"),
656
684
  ):
657
685
  if key in info:
658
686
  attr[name] = int(info[key] or 0)
659
687
  for key, name in (
688
+ ("fl", "labels"),
660
689
  ("dp", "dir_path"),
661
690
  ("style", "style"),
662
691
  ("ns", "name_show"),
@@ -744,10 +773,7 @@ def normalize_attr_app(
744
773
  if "upt" in info:
745
774
  attr["mtime"] = int(info["upt"])
746
775
  else:
747
- attr["labels"] = info["fl"]
748
776
  attr["ico"] = info.get("ico", "folder" if attr["is_dir"] else "")
749
- if "ftype" in info:
750
- attr["file_type"] = int(info["ftype"] or 0)
751
777
  if "thumb" in info:
752
778
  thumb = info["thumb"]
753
779
  if thumb.startswith("?"):
@@ -763,6 +789,7 @@ def normalize_attr_app(
763
789
  ("aid", "area_id"),
764
790
  ("fatr", "audio_play_long"),
765
791
  ("fta", "status"),
792
+ ("ftype", "file_type"),
766
793
  ("ism", "star"),
767
794
  ("ism", "is_mark"),
768
795
  ("is_top", "is_top"),
@@ -784,6 +811,7 @@ def normalize_attr_app(
784
811
  ("fco", "cover"),
785
812
  ("fco", "folder_cover"),
786
813
  ("fdesc", "desc"),
814
+ ("fl", "labels"),
787
815
  ("flabel", "fflabel"),
788
816
  ("multitrack", "multitrack"),
789
817
  ("play_long", "play_long"),
@@ -838,18 +866,28 @@ def normalize_attr_app2(
838
866
  else:
839
867
  dict_cls = AttrDict
840
868
  attr: dict[str, Any] = dict_cls()
841
- is_directory = attr["is_dir"] = "file_id" not in info
842
- if not simple:
843
- attr["is_directory"] = is_directory
844
- if is_directory:
869
+ if "file_id" in info and "parent_id" in info:
870
+ if "file_category" in info:
871
+ is_directory = not int(info["file_category"])
872
+ else:
873
+ is_directory = bool(info.get("sha1") or info.get("file_sha1"))
845
874
  attr["id"] = int(info["file_id"])
846
- attr["parent_id"] = int(info["category_id"])
875
+ attr["parent_id"] = int(info["parent_id"])
847
876
  attr["name"] = info["file_name"]
848
877
  else:
849
- attr["id"] = int(info["category_id"])
850
- attr["parent_id"] = int(info["parent_id"])
851
- attr["name"] = info["category_name"]
852
- attr["sha1"] = info.get("sha1") or ""
878
+ is_directory = "file_id" not in info
879
+ if is_directory:
880
+ attr["id"] = int(info["file_id"])
881
+ attr["parent_id"] = int(info["category_id"])
882
+ attr["name"] = info["file_name"]
883
+ else:
884
+ attr["id"] = int(info["category_id"])
885
+ attr["parent_id"] = int(info["parent_id"])
886
+ attr["name"] = info["category_name"]
887
+ attr["is_dir"] = is_directory
888
+ if not simple:
889
+ attr["is_directory"] = is_directory
890
+ attr["sha1"] = info.get("sha1") or info.get("file_sha1") or ""
853
891
  attr["size"] = int(info.get("file_size") or 0)
854
892
  if "pick_code" in info:
855
893
  attr["pickcode"] = info["pick_code"]
@@ -890,7 +928,8 @@ def normalize_attr_app2(
890
928
  if "utime" in info:
891
929
  attr["utime"] = int(info["utime"])
892
930
  attr["ico"] = info.get("ico", "folder" if attr["is_dir"] else "")
893
- attr["labels"] = info["fl"]
931
+ if "fl" in info:
932
+ attr["labels"] = info["fl"]
894
933
  for key, name in (
895
934
  ("area_id", "area_id"),
896
935
  ("has_desc", "has_desc"),
@@ -1158,13 +1197,17 @@ class ClientRequestMixin:
1158
1197
  "user-agent": "Mozilla/5.0 AppleWebKit/600 Safari/600 Chrome/124.0.0.0",
1159
1198
  })
1160
1199
 
1200
+ @locked_cacheproperty
1201
+ def request_kwargs(self, /) -> dict:
1202
+ return {}
1203
+
1161
1204
  def close(self, /) -> None:
1162
1205
  """删除 session 和 async_session 属性,如果它们未被引用,则应该会被自动清理
1163
1206
  """
1164
1207
  self.__dict__.pop("session", None)
1165
1208
  self.__dict__.pop("async_session", None)
1166
1209
 
1167
- def request(
1210
+ def _request(
1168
1211
  self,
1169
1212
  /,
1170
1213
  url: str,
@@ -1282,120 +1325,252 @@ class ClientRequestMixin:
1282
1325
  request_kwargs.setdefault("parse", default_parse)
1283
1326
  return request(url=url, method=method, **request_kwargs)
1284
1327
 
1328
+ def request(
1329
+ self,
1330
+ /,
1331
+ url: str,
1332
+ method: str = "GET",
1333
+ params = None,
1334
+ data = None,
1335
+ *,
1336
+ async_: Literal[False, True] = False,
1337
+ **request_kwargs,
1338
+ ):
1339
+ """帮助函数:可执行同步和异步的网络请求
1340
+
1341
+ :param url: HTTP 的请求链接
1342
+ :param method: HTTP 的请求方法
1343
+ :param params: 查询参数
1344
+ :param ecdh_encrypt: 使用 ecdh 算法进行加密(返回值也要解密)
1345
+ :param async_: 说明 `request` 是同步调用还是异步调用
1346
+ :param request: HTTP 请求调用,如果为 None,则默认用 httpx 执行请求
1347
+ 如果传入调用,则必须至少能接受以下几个关键词参数:
1348
+
1349
+ - url: HTTP 的请求链接
1350
+ - method: HTTP 的请求方法
1351
+ - headers: HTTP 的请求头
1352
+ - data: HTTP 的请求体
1353
+ - parse: 解析 HTTP 响应的方法,默认会构建一个 Callable,会把响应的字节数据视为 JSON 进行反序列化解析
1354
+
1355
+ - 如果为 None,则直接把响应对象返回
1356
+ - 如果为 ...(Ellipsis),则把响应对象关闭后将其返回
1357
+ - 如果为 True,则根据响应头来确定把响应得到的字节数据解析成何种格式(反序列化),请求也会被自动关闭
1358
+ - 如果为 False,则直接返回响应得到的字节数据,请求也会被自动关闭
1359
+ - 如果为 Callable,则使用此调用来解析数据,接受 1-2 个位置参数,并把解析结果返回给 `request` 的调用者,请求也会被自动关闭
1360
+ - 如果只接受 1 个位置参数,则把响应对象传给它
1361
+ - 如果能接受 2 个位置参数,则把响应对象和响应得到的字节数据(响应体)传给它
1362
+
1363
+ :param request_kwargs: 其余的请求参数,会被传给 `request`
1364
+
1365
+ :return: 直接返回 `request` 执行请求后的返回值
1366
+
1367
+ .. note::
1368
+ `request` 可以由不同的请求库来提供,下面是封装了一些模块
1369
+
1370
+ 1. `httpx_request <https://pypi.org/project/httpx_request/>`_,由 `httpx <https://pypi.org/project/httpx/>`_ 封装,支持同步和异步调用,本模块默认用的就是这个封装
1371
+
1372
+ .. code:: python
1373
+
1374
+ from httpx_request import request
1375
+
1376
+ 2. `python-urlopen <https://pypi.org/project/python-urlopen/>`_,由 `urllib.request.urlopen <https://docs.python.org/3/library/urllib.request.html#urllib.request.urlopen>`_ 封装,支持同步调用,性能相对最差
1377
+
1378
+ .. code:: python
1379
+
1380
+ from urlopen import request
1381
+
1382
+ 3. `urllib3_request <https://pypi.org/project/urllib3_request/>`_,由 `urllib3 <https://pypi.org/project/urllib3/>`_ 封装,支持同步调用,性能相对较好,推荐使用
1383
+
1384
+ .. code:: python
1385
+
1386
+ from urllib3_request import request
1387
+
1388
+ 4. `requests_request <https://pypi.org/project/requests_request/>`_,由 `requests <https://pypi.org/project/requests/>`_ 封装,支持同步调用
1389
+
1390
+ .. code:: python
1391
+
1392
+ from requests_request import request
1393
+
1394
+ 5. `aiohttp_client_request <https://pypi.org/project/aiohttp_client_request/>`_,由 `aiohttp <https://pypi.org/project/aiohttp/>`_ 封装,支持异步调用,异步并发能力最强,推荐使用
1395
+
1396
+ .. code:: python
1397
+
1398
+ from aiohttp_client_request import request
1399
+
1400
+ 6. `blacksheep_client_request <https://pypi.org/project/blacksheep_client_request/>`_,由 `blacksheep <https://pypi.org/project/blacksheep/>`_ 封装,支持异步调用
1401
+
1402
+ .. code:: python
1403
+
1404
+ from blacksheep_client_request import request
1405
+ """
1406
+ kwargs = {**self.request_kwargs, **request_kwargs}
1407
+ return self._request(url, method, params, data, async_=async_, **kwargs)
1408
+
1285
1409
  ########## Qrcode API ##########
1286
1410
 
1411
+ @overload
1412
+ def login_authorize_open(
1413
+ self,
1414
+ payload: dict,
1415
+ /,
1416
+ base_url: str | Callable[[], str] = "https://qrcodeapi.115.com",
1417
+ *,
1418
+ async_: Literal[False] = False,
1419
+ **request_kwargs,
1420
+ ) -> dict:
1421
+ ...
1422
+ @overload
1423
+ def login_authorize_open(
1424
+ self,
1425
+ payload: dict,
1426
+ /,
1427
+ base_url: str | Callable[[], str] = "https://qrcodeapi.115.com",
1428
+ *,
1429
+ async_: Literal[True],
1430
+ **request_kwargs,
1431
+ ) -> Coroutine[Any, Any, dict]:
1432
+ ...
1433
+ def login_authorize_open(
1434
+ self,
1435
+ payload: dict,
1436
+ /,
1437
+ base_url: str | Callable[[], str] = "https://qrcodeapi.115.com",
1438
+ *,
1439
+ async_: Literal[False, True] = False,
1440
+ **request_kwargs,
1441
+ ) -> dict | Coroutine[Any, Any, dict]:
1442
+ """授权码方式请求开放接口应用授权
1443
+
1444
+ GET https://qrcodeapi.115.com/open/authorize
1445
+
1446
+ .. admonition:: Reference
1447
+
1448
+ https://www.yuque.com/115yun/open/okr2cq0wywelscpe#EiOrD
1449
+
1450
+ :payload:
1451
+ - client_id: int | str 💡 AppID
1452
+ - redirect_uri: str 💡 授权成功后重定向到指定的地址并附上授权码 code,需要先到 https://open.115.com/ 应用管理应用域名设置
1453
+ - response_type: str = "code" 💡 授权模式,固定为 code,表示授权码模式
1454
+ - state: int | str = <default> 💡 随机值,会通过 redirect_uri 原样返回,可用于验证以防 MITM 和 CSRF
1455
+ """
1456
+ api = complete_api("/open/authorize", base_url=base_url)
1457
+ payload = {"response_type": "code", **payload}
1458
+ return self.request(url=api, params=payload, async_=async_, **request_kwargs)
1459
+
1287
1460
  @overload
1288
1461
  @staticmethod
1289
- def login_qrcode(
1290
- payload: str | dict,
1462
+ def login_authorize_access_token_open(
1463
+ payload: dict,
1291
1464
  /,
1292
1465
  request: None | Callable = None,
1293
- app: str = "web",
1294
1466
  base_url: str | Callable[[], str] = "https://qrcodeapi.115.com",
1295
1467
  *,
1296
1468
  async_: Literal[False] = False,
1297
1469
  **request_kwargs,
1298
- ) -> bytes:
1470
+ ) -> dict:
1299
1471
  ...
1300
1472
  @overload
1301
1473
  @staticmethod
1302
- def login_qrcode(
1303
- payload: str | dict,
1474
+ def login_authorize_access_token_open(
1475
+ payload: dict,
1304
1476
  /,
1305
1477
  request: None | Callable = None,
1306
- app: str = "web",
1307
1478
  base_url: str | Callable[[], str] = "https://qrcodeapi.115.com",
1308
1479
  *,
1309
1480
  async_: Literal[True],
1310
1481
  **request_kwargs,
1311
- ) -> Coroutine[Any, Any, bytes]:
1482
+ ) -> Coroutine[Any, Any, dict]:
1312
1483
  ...
1313
1484
  @staticmethod
1314
- def login_qrcode(
1315
- payload: str | dict,
1485
+ def login_authorize_access_token_open(
1486
+ payload: dict,
1316
1487
  /,
1317
1488
  request: None | Callable = None,
1318
- app: str = "web",
1319
1489
  base_url: str | Callable[[], str] = "https://qrcodeapi.115.com",
1320
1490
  *,
1321
1491
  async_: Literal[False, True] = False,
1322
1492
  **request_kwargs,
1323
- ) -> bytes | Coroutine[Any, Any, bytes]:
1324
- """下载登录二维码图片
1493
+ ) -> dict | Coroutine[Any, Any, dict]:
1494
+ """用授权码获取开放接口应用的 access_token
1325
1495
 
1326
- GET https://qrcodeapi.115.com/api/1.0/web/1.0/qrcode
1496
+ POST https://qrcodeapi.115.com/open/authCodeToToken
1327
1497
 
1328
- :params uid: 二维码的 uid
1498
+ .. admonition:: Reference
1329
1499
 
1330
- :return: 图片的二进制数据(PNG 图片)
1500
+ https://www.yuque.com/115yun/open/okr2cq0wywelscpe#JnDgl
1501
+
1502
+ :payload:
1503
+ - client_id: int | str 💡 AppID
1504
+ - client_secret: str 💡 AppSecret
1505
+ - code: str 💡 授权码,/open/authCodeToToken 重定向地址里面
1506
+ - redirect_uri: str 💡 与 /open/authCodeToToken 传的 redirect_uri 一致,可用于验证以防 MITM 和 CSRF
1507
+ - grant_type: str = "authorization_code" 💡 授权类型,固定为 authorization_code,表示授权码类型
1331
1508
  """
1332
- api = complete_api(f"/api/1.0/{app}/1.0/qrcode", base_url=base_url)
1333
- if isinstance(payload, str):
1334
- payload = {"uid": payload}
1335
- request_kwargs.setdefault("parse", False)
1509
+ api = complete_api("/open/authCodeToToken", base_url=base_url)
1510
+ payload = {"grant_type": "authorization_code", **payload}
1511
+ request_kwargs.setdefault("parse", default_parse)
1336
1512
  if request is None:
1337
- return get_default_request()(url=api, params=payload, async_=async_, **request_kwargs)
1513
+ return get_default_request()(url=api, method="POST", data=payload, async_=async_, **request_kwargs)
1338
1514
  else:
1339
- return request(url=api, params=payload, **request_kwargs)
1515
+ return request(url=api, method="POST", data=payload, **request_kwargs)
1340
1516
 
1341
1517
  @overload
1342
1518
  @staticmethod
1343
- def login_qrcode_access_token_open(
1519
+ def login_qrcode(
1344
1520
  payload: str | dict,
1345
1521
  /,
1346
1522
  request: None | Callable = None,
1523
+ app: str = "web",
1347
1524
  base_url: str | Callable[[], str] = "https://qrcodeapi.115.com",
1348
1525
  *,
1349
1526
  async_: Literal[False] = False,
1350
1527
  **request_kwargs,
1351
- ) -> dict:
1528
+ ) -> bytes:
1352
1529
  ...
1353
1530
  @overload
1354
1531
  @staticmethod
1355
- def login_qrcode_access_token_open(
1532
+ def login_qrcode(
1356
1533
  payload: str | dict,
1357
1534
  /,
1358
1535
  request: None | Callable = None,
1536
+ app: str = "web",
1359
1537
  base_url: str | Callable[[], str] = "https://qrcodeapi.115.com",
1360
1538
  *,
1361
1539
  async_: Literal[True],
1362
1540
  **request_kwargs,
1363
- ) -> Coroutine[Any, Any, dict]:
1541
+ ) -> Coroutine[Any, Any, bytes]:
1364
1542
  ...
1365
1543
  @staticmethod
1366
- def login_qrcode_access_token_open(
1544
+ def login_qrcode(
1367
1545
  payload: str | dict,
1368
1546
  /,
1369
1547
  request: None | Callable = None,
1548
+ app: str = "web",
1370
1549
  base_url: str | Callable[[], str] = "https://qrcodeapi.115.com",
1371
1550
  *,
1372
1551
  async_: Literal[False, True] = False,
1373
1552
  **request_kwargs,
1374
- ) -> dict | Coroutine[Any, Any, dict]:
1375
- """绑定扫码并获取开放平台应用的 access_token 和 refresh_token
1376
-
1377
- POST https://qrcodeapi.115.com/open/deviceCodeToToken
1553
+ ) -> bytes | Coroutine[Any, Any, bytes]:
1554
+ """下载登录二维码图片
1378
1555
 
1379
- .. admonition:: Reference
1556
+ GET https://qrcodeapi.115.com/api/1.0/web/1.0/qrcode
1380
1557
 
1381
- https://www.yuque.com/115yun/open/shtpzfhewv5nag11#QCCVQ
1558
+ :params uid: 二维码的 uid
1382
1559
 
1383
- :payload:
1384
- - uid: str
1385
- - code_verifier: str = <default> 💡 默认字符串是 64 个 "0"
1560
+ :return: 图片的二进制数据(PNG 图片)
1386
1561
  """
1387
- api = complete_api("/open/deviceCodeToToken", base_url=base_url)
1562
+ api = complete_api(f"/api/1.0/{app}/1.0/qrcode", base_url=base_url)
1388
1563
  if isinstance(payload, str):
1389
- payload = {"uid": payload, "code_verifier": _default_code_verifier}
1390
- request_kwargs.setdefault("parse", default_parse)
1564
+ payload = {"uid": payload}
1565
+ request_kwargs.setdefault("parse", False)
1391
1566
  if request is None:
1392
- return get_default_request()(url=api, method="POST", data=payload, async_=async_, **request_kwargs)
1567
+ return get_default_request()(url=api, params=payload, async_=async_, **request_kwargs)
1393
1568
  else:
1394
- return request(url=api, method="POST", data=payload, **request_kwargs)
1569
+ return request(url=api, params=payload, **request_kwargs)
1395
1570
 
1396
1571
  @overload
1397
1572
  @staticmethod
1398
- def login_qrcode_refresh_token_open(
1573
+ def login_qrcode_access_token_open(
1399
1574
  payload: str | dict,
1400
1575
  /,
1401
1576
  request: None | Callable = None,
@@ -1407,7 +1582,7 @@ class ClientRequestMixin:
1407
1582
  ...
1408
1583
  @overload
1409
1584
  @staticmethod
1410
- def login_qrcode_refresh_token_open(
1585
+ def login_qrcode_access_token_open(
1411
1586
  payload: str | dict,
1412
1587
  /,
1413
1588
  request: None | Callable = None,
@@ -1418,7 +1593,7 @@ class ClientRequestMixin:
1418
1593
  ) -> Coroutine[Any, Any, dict]:
1419
1594
  ...
1420
1595
  @staticmethod
1421
- def login_qrcode_refresh_token_open(
1596
+ def login_qrcode_access_token_open(
1422
1597
  payload: str | dict,
1423
1598
  /,
1424
1599
  request: None | Callable = None,
@@ -1427,20 +1602,21 @@ class ClientRequestMixin:
1427
1602
  async_: Literal[False, True] = False,
1428
1603
  **request_kwargs,
1429
1604
  ) -> dict | Coroutine[Any, Any, dict]:
1430
- """用一个 refresh_token 去获取新的 access_token 和 refresh_token,然后原来的 refresh_token 作废
1605
+ """绑定扫码并获取开放平台应用的 access_token 和 refresh_token
1431
1606
 
1432
- POST https://qrcodeapi.115.com/open/refreshToken
1607
+ POST https://qrcodeapi.115.com/open/deviceCodeToToken
1433
1608
 
1434
1609
  .. admonition:: Reference
1435
1610
 
1436
- https://www.yuque.com/115yun/open/shtpzfhewv5nag11#ve54x
1611
+ https://www.yuque.com/115yun/open/shtpzfhewv5nag11#QCCVQ
1437
1612
 
1438
1613
  :payload:
1439
- - refresh_token: str
1614
+ - uid: str
1615
+ - code_verifier: str = <default> 💡 默认字符串是 64 个 "0"
1440
1616
  """
1441
- api = complete_api("/open/refreshToken", base_url=base_url)
1617
+ api = complete_api("/open/deviceCodeToToken", base_url=base_url)
1442
1618
  if isinstance(payload, str):
1443
- payload = {"refresh_token": payload}
1619
+ payload = {"uid": payload, "code_verifier": _default_code_verifier}
1444
1620
  request_kwargs.setdefault("parse", default_parse)
1445
1621
  if request is None:
1446
1622
  return get_default_request()(url=api, method="POST", data=payload, async_=async_, **request_kwargs)
@@ -1766,7 +1942,7 @@ class ClientRequestMixin:
1766
1942
  async_: Literal[False, True] = False,
1767
1943
  **request_kwargs,
1768
1944
  ) -> dict | Coroutine[Any, Any, dict]:
1769
- """获取开放平台的登录二维码,扫码可用
1945
+ """获取开放平台的登录二维码,扫码可用,采用 PKCE (Proof Key for Code Exchange)
1770
1946
 
1771
1947
  POST https://qrcodeapi.115.com/open/authDeviceCode
1772
1948
 
@@ -1807,12 +1983,11 @@ class ClientRequestMixin:
1807
1983
  return request(url=api, method="POST", data=payload, **request_kwargs)
1808
1984
 
1809
1985
  @overload
1810
- @classmethod
1811
- def login_with_qrcode(
1812
- cls,
1986
+ @staticmethod
1987
+ def login_refresh_token_open(
1988
+ payload: str | dict,
1813
1989
  /,
1814
- app: None | str = "",
1815
- console_qrcode: bool = True,
1990
+ request: None | Callable = None,
1816
1991
  base_url: str | Callable[[], str] = "https://qrcodeapi.115.com",
1817
1992
  *,
1818
1993
  async_: Literal[False] = False,
@@ -1820,24 +1995,81 @@ class ClientRequestMixin:
1820
1995
  ) -> dict:
1821
1996
  ...
1822
1997
  @overload
1823
- @classmethod
1824
- def login_with_qrcode(
1825
- cls,
1998
+ @staticmethod
1999
+ def login_refresh_token_open(
2000
+ payload: str | dict,
1826
2001
  /,
1827
- app: None | str = "",
1828
- console_qrcode: bool = True,
2002
+ request: None | Callable = None,
1829
2003
  base_url: str | Callable[[], str] = "https://qrcodeapi.115.com",
1830
2004
  *,
1831
2005
  async_: Literal[True],
1832
2006
  **request_kwargs,
1833
2007
  ) -> Coroutine[Any, Any, dict]:
1834
2008
  ...
1835
- @classmethod
1836
- def login_with_qrcode(
1837
- cls,
2009
+ @staticmethod
2010
+ def login_refresh_token_open(
2011
+ payload: str | dict,
1838
2012
  /,
1839
- app: None | str = "",
1840
- console_qrcode: bool = True,
2013
+ request: None | Callable = None,
2014
+ base_url: str | Callable[[], str] = "https://qrcodeapi.115.com",
2015
+ *,
2016
+ async_: Literal[False, True] = False,
2017
+ **request_kwargs,
2018
+ ) -> dict | Coroutine[Any, Any, dict]:
2019
+ """用一个 refresh_token 去获取新的 access_token 和 refresh_token,然后原来的 refresh_token 作废
2020
+
2021
+ POST https://qrcodeapi.115.com/open/refreshToken
2022
+
2023
+ .. admonition:: Reference
2024
+
2025
+ https://www.yuque.com/115yun/open/shtpzfhewv5nag11#ve54x
2026
+
2027
+ https://www.yuque.com/115yun/open/opnx8yezo4at2be6
2028
+
2029
+ :payload:
2030
+ - refresh_token: str
2031
+ """
2032
+ api = complete_api("/open/refreshToken", base_url=base_url)
2033
+ if isinstance(payload, str):
2034
+ payload = {"refresh_token": payload}
2035
+ request_kwargs.setdefault("parse", default_parse)
2036
+ if request is None:
2037
+ return get_default_request()(url=api, method="POST", data=payload, async_=async_, **request_kwargs)
2038
+ else:
2039
+ return request(url=api, method="POST", data=payload, **request_kwargs)
2040
+
2041
+ @overload
2042
+ @classmethod
2043
+ def login_with_qrcode(
2044
+ cls,
2045
+ /,
2046
+ app: None | str = "",
2047
+ console_qrcode: bool = True,
2048
+ base_url: str | Callable[[], str] = "https://qrcodeapi.115.com",
2049
+ *,
2050
+ async_: Literal[False] = False,
2051
+ **request_kwargs,
2052
+ ) -> dict:
2053
+ ...
2054
+ @overload
2055
+ @classmethod
2056
+ def login_with_qrcode(
2057
+ cls,
2058
+ /,
2059
+ app: None | str = "",
2060
+ console_qrcode: bool = True,
2061
+ base_url: str | Callable[[], str] = "https://qrcodeapi.115.com",
2062
+ *,
2063
+ async_: Literal[True],
2064
+ **request_kwargs,
2065
+ ) -> Coroutine[Any, Any, dict]:
2066
+ ...
2067
+ @classmethod
2068
+ def login_with_qrcode(
2069
+ cls,
2070
+ /,
2071
+ app: None | str = "",
2072
+ console_qrcode: bool = True,
1841
2073
  base_url: str | Callable[[], str] = "https://qrcodeapi.115.com",
1842
2074
  *,
1843
2075
  async_: Literal[False, True] = False,
@@ -1955,7 +2187,7 @@ class ClientRequestMixin:
1955
2187
  else:
1956
2188
  url = complete_api(f"/api/1.0/web/1.0/qrcode?uid={login_uid}", base_url=base_url)
1957
2189
  if async_:
1958
- yield partial(startfile_async, url)
2190
+ yield startfile_async(url)
1959
2191
  else:
1960
2192
  startfile(url)
1961
2193
  while True:
@@ -1992,7 +2224,7 @@ class ClientRequestMixin:
1992
2224
  )
1993
2225
  else:
1994
2226
  return qrcode_token
1995
- return run_gen_step(gen_step, async_=async_)
2227
+ return run_gen_step(gen_step, simple=True, async_=async_)
1996
2228
 
1997
2229
  @overload
1998
2230
  @classmethod
@@ -2059,7 +2291,7 @@ class ClientRequestMixin:
2059
2291
  else:
2060
2292
  url = complete_api(f"/api/1.0/web/1.0/qrcode?uid={login_uid}", base_url=base_url)
2061
2293
  if async_:
2062
- yield partial(startfile_async, url)
2294
+ yield startfile_async(url)
2063
2295
  else:
2064
2296
  startfile(url)
2065
2297
  while True:
@@ -2092,7 +2324,7 @@ class ClientRequestMixin:
2092
2324
  async_=async_,
2093
2325
  **request_kwargs,
2094
2326
  )
2095
- return run_gen_step(gen_step, async_=async_)
2327
+ return run_gen_step(gen_step, simple=True, async_=async_)
2096
2328
 
2097
2329
  ########## Upload API ##########
2098
2330
 
@@ -2475,7 +2707,7 @@ class ClientRequestMixin:
2475
2707
  async_=async_,
2476
2708
  **request_kwargs,
2477
2709
  )
2478
- return run_gen_step(gen_step, async_=async_)
2710
+ return run_gen_step(gen_step, simple=True, async_=async_)
2479
2711
 
2480
2712
  @overload
2481
2713
  def read_bytes_range(
@@ -2590,7 +2822,7 @@ class ClientRequestMixin:
2590
2822
  async_=async_,
2591
2823
  **request_kwargs,
2592
2824
  )
2593
- return run_gen_step(gen_step, async_=async_)
2825
+ return run_gen_step(gen_step, simple=True, async_=async_)
2594
2826
 
2595
2827
 
2596
2828
  class P115OpenClient(ClientRequestMixin):
@@ -2668,7 +2900,7 @@ class P115OpenClient(ClientRequestMixin):
2668
2900
  app_id_or_refresh_token.startswith("0") or
2669
2901
  app_id_or_refresh_token.strip(digits)
2670
2902
  ):
2671
- resp = yield self.login_qrcode_refresh_token_open(
2903
+ resp = yield self.login_refresh_token_open(
2672
2904
  app_id_or_refresh_token,
2673
2905
  async_=async_,
2674
2906
  **request_kwargs,
@@ -2686,7 +2918,7 @@ class P115OpenClient(ClientRequestMixin):
2686
2918
  self.refresh_token = data["refresh_token"]
2687
2919
  self.access_token = data["access_token"]
2688
2920
  return self
2689
- return run_gen_step(gen_step, async_=async_)
2921
+ return run_gen_step(gen_step, simple=True, async_=async_)
2690
2922
 
2691
2923
  @classmethod
2692
2924
  def from_token(cls, /, access_token: str, refresh_token: str) -> P115OpenClient:
@@ -2743,7 +2975,7 @@ class P115OpenClient(ClientRequestMixin):
2743
2975
  """更新 access_token 和 refresh_token (⚠️ 目前是 7200 秒内就要求刷新一次)
2744
2976
  """
2745
2977
  def gen_step():
2746
- resp = yield self.login_qrcode_refresh_token_open(
2978
+ resp = yield self.login_refresh_token_open(
2747
2979
  self.refresh_token,
2748
2980
  async_=async_,
2749
2981
  **request_kwargs,
@@ -2753,7 +2985,7 @@ class P115OpenClient(ClientRequestMixin):
2753
2985
  self.refresh_token = data["refresh_token"]
2754
2986
  access_token = self.access_token = data["access_token"]
2755
2987
  return access_token
2756
- return run_gen_step(gen_step, async_=async_)
2988
+ return run_gen_step(gen_step, simple=True, async_=async_)
2757
2989
 
2758
2990
  @overload
2759
2991
  def download_url(
@@ -3136,9 +3368,675 @@ class P115OpenClient(ClientRequestMixin):
3136
3368
  return self.request(url=api, params=payload, async_=async_, **request_kwargs)
3137
3369
 
3138
3370
  @overload
3139
- def fs_info(
3371
+ def fs_info(
3372
+ self,
3373
+ payload: int | str | dict,
3374
+ /,
3375
+ base_url: bool | str | Callable[[], str] = False,
3376
+ *,
3377
+ async_: Literal[False] = False,
3378
+ **request_kwargs,
3379
+ ) -> dict:
3380
+ ...
3381
+ @overload
3382
+ def fs_info(
3383
+ self,
3384
+ payload: int | str | dict,
3385
+ /,
3386
+ base_url: bool | str | Callable[[], str] = False,
3387
+ *,
3388
+ async_: Literal[True],
3389
+ **request_kwargs,
3390
+ ) -> Coroutine[Any, Any, dict]:
3391
+ ...
3392
+ def fs_info(
3393
+ self,
3394
+ payload: int | str | dict,
3395
+ /,
3396
+ base_url: bool | str | Callable[[], str] = False,
3397
+ *,
3398
+ async_: Literal[False, True] = False,
3399
+ **request_kwargs,
3400
+ ) -> dict | Coroutine[Any, Any, dict]:
3401
+ """获取文件或目录详情
3402
+
3403
+ GET https://proapi.115.com/open/folder/get_info
3404
+
3405
+ .. hint::
3406
+ 相当于 `P115Client.fs_category_get_app`
3407
+
3408
+ .. admonition:: Reference
3409
+
3410
+ https://www.yuque.com/115yun/open/rl8zrhe2nag21dfw
3411
+
3412
+ :payload:
3413
+ - file_id: int | str 💡 文件或目录的 id
3414
+ """
3415
+ api = complete_proapi("/open/folder/get_info", base_url)
3416
+ if isinstance(payload, (int, str)):
3417
+ payload = {"file_id": payload}
3418
+ return self.request(url=api, params=payload, async_=async_, **request_kwargs)
3419
+
3420
+ @overload
3421
+ def fs_mkdir(
3422
+ self,
3423
+ payload: str | dict,
3424
+ /,
3425
+ pid: int = 0,
3426
+ base_url: bool | str | Callable[[], str] = False,
3427
+ *,
3428
+ async_: Literal[False] = False,
3429
+ **request_kwargs,
3430
+ ) -> dict:
3431
+ ...
3432
+ @overload
3433
+ def fs_mkdir(
3434
+ self,
3435
+ payload: str | dict,
3436
+ /,
3437
+ pid: int = 0,
3438
+ base_url: bool | str | Callable[[], str] = False,
3439
+ *,
3440
+ async_: Literal[True],
3441
+ **request_kwargs,
3442
+ ) -> Coroutine[Any, Any, dict]:
3443
+ ...
3444
+ def fs_mkdir(
3445
+ self,
3446
+ payload: str | dict,
3447
+ /,
3448
+ pid: int = 0,
3449
+ base_url: bool | str | Callable[[], str] = False,
3450
+ *,
3451
+ async_: Literal[False, True] = False,
3452
+ **request_kwargs,
3453
+ ) -> dict | Coroutine[Any, Any, dict]:
3454
+ """新建目录
3455
+
3456
+ POST https://proapi.115.com/open/folder/add
3457
+
3458
+ .. admonition:: Reference
3459
+
3460
+ https://www.yuque.com/115yun/open/qur839kyx9cgxpxi
3461
+
3462
+ :payload:
3463
+ - file_name: str 💡 新建目录名称,限制255个字符
3464
+ - pid: int | str = 0 💡 新建目录所在的父目录ID (根目录的ID为0)
3465
+ """
3466
+ api = complete_proapi("/open/folder/add", base_url)
3467
+ if isinstance(payload, str):
3468
+ payload = {"pid": pid, "file_name": payload}
3469
+ else:
3470
+ payload = {"pid": pid, **payload}
3471
+ return self.request(url=api, method="POST", data=payload, async_=async_, **request_kwargs)
3472
+
3473
+ @overload
3474
+ def fs_move(
3475
+ self,
3476
+ payload: int | str | Iterable[int | str] | dict,
3477
+ /,
3478
+ pid: int = 0,
3479
+ base_url: bool | str | Callable[[], str] = False,
3480
+ *,
3481
+ async_: Literal[False] = False,
3482
+ **request_kwargs,
3483
+ ) -> dict:
3484
+ ...
3485
+ @overload
3486
+ def fs_move(
3487
+ self,
3488
+ payload: int | str | Iterable[int | str] | dict,
3489
+ /,
3490
+ pid: int = 0,
3491
+ base_url: bool | str | Callable[[], str] = False,
3492
+ *,
3493
+ async_: Literal[True],
3494
+ **request_kwargs,
3495
+ ) -> Coroutine[Any, Any, dict]:
3496
+ ...
3497
+ def fs_move(
3498
+ self,
3499
+ payload: int | str | Iterable[int | str] | dict,
3500
+ /,
3501
+ pid: int = 0,
3502
+ base_url: bool | str | Callable[[], str] = False,
3503
+ *,
3504
+ async_: Literal[False, True] = False,
3505
+ **request_kwargs,
3506
+ ) -> dict | Coroutine[Any, Any, dict]:
3507
+ """文件移动
3508
+
3509
+ POST https://proapi.115.com/open/ufile/move
3510
+
3511
+ .. admonition:: Reference
3512
+
3513
+ https://www.yuque.com/115yun/open/vc6fhi2mrkenmav2
3514
+
3515
+ :payload:
3516
+ - file_ids: int | str 💡 文件或目录的 id,多个用逗号 "," 隔开
3517
+ - to_cid: int | str = 0 💡 父目录 id
3518
+ """
3519
+ api = complete_proapi("/open/ufile/move", base_url)
3520
+ if isinstance(payload, (int, str)):
3521
+ payload = {"file_ids": payload}
3522
+ elif isinstance(payload, dict):
3523
+ payload = dict(payload)
3524
+ else:
3525
+ payload = {"file_ids": ",".join(map(str, payload))}
3526
+ if not payload.get("file_ids"):
3527
+ return {"state": False, "message": "no op"}
3528
+ payload = cast(dict, payload)
3529
+ payload.setdefault("to_cid", pid)
3530
+ return self.request(url=api, method="POST", data=payload, async_=async_, **request_kwargs)
3531
+
3532
+ @overload
3533
+ def fs_search(
3534
+ self,
3535
+ payload: str | dict = ".",
3536
+ /,
3537
+ base_url: bool | str | Callable[[], str] = False,
3538
+ *,
3539
+ async_: Literal[False] = False,
3540
+ **request_kwargs,
3541
+ ) -> dict:
3542
+ ...
3543
+ @overload
3544
+ def fs_search(
3545
+ self,
3546
+ payload: str | dict = ".",
3547
+ /,
3548
+ base_url: bool | str | Callable[[], str] = False,
3549
+ *,
3550
+ async_: Literal[True],
3551
+ **request_kwargs,
3552
+ ) -> Coroutine[Any, Any, dict]:
3553
+ ...
3554
+ def fs_search(
3555
+ self,
3556
+ payload: str | dict = ".",
3557
+ /,
3558
+ base_url: bool | str | Callable[[], str] = False,
3559
+ *,
3560
+ async_: Literal[False, True] = False,
3561
+ **request_kwargs,
3562
+ ) -> dict | Coroutine[Any, Any, dict]:
3563
+ """搜索文件或目录
3564
+
3565
+ GET https://proapi.115.com/open/ufile/search
3566
+
3567
+ .. hint::
3568
+ 相当于 `P115Client.fs_search_app2`
3569
+
3570
+ .. admonition:: Reference
3571
+
3572
+ https://www.yuque.com/115yun/open/ft2yelxzopusus38
3573
+
3574
+ :payload:
3575
+ - aid: int | str = 1 💡 area_id。1:正常文件 7:回收站文件 12:瞬间文件 120:彻底删除文件、简历附件
3576
+ - asc: 0 | 1 = <default> 💡 是否升序排列
3577
+ - cid: int | str = 0 💡 目录 id。cid=-1 时,表示不返回列表任何内容
3578
+ - count_folders: 0 | 1 = <default>
3579
+ - date: str = <default> 💡 筛选日期
3580
+ - fc: 0 | 1 = <default> 💡 只显示文件或目录。1:只显示目录 2:只显示文件
3581
+ - fc_mix: 0 | 1 = <default> 💡 是否目录和文件混合,如果为 0 则目录在前(目录置顶)
3582
+ - file_label: int | str = <default> 💡 标签 id
3583
+ - format: str = "json" 💡 输出格式(不用管)
3584
+ - gte_day: str 💡 搜索结果匹配的开始时间;格式:YYYY-MM-DD
3585
+ - limit: int = 32 💡 一页大小,意思就是 page_size
3586
+ - lte_day: str 💡 搜索结果匹配的结束时间;格式:YYYY-MM-DD
3587
+ - o: str = <default> 💡 用某字段排序
3588
+
3589
+ - "file_name": 文件名
3590
+ - "file_size": 文件大小
3591
+ - "file_type": 文件种类
3592
+ - "user_utime": 修改时间
3593
+ - "user_ptime": 创建时间
3594
+ - "user_otime": 上一次打开时间
3595
+
3596
+ - offset: int = 0 💡 索引偏移,索引从 0 开始计算
3597
+ - pick_code: str = <default> 💡 是否查询提取码,如果该值为 1 则查询提取码为 `search_value` 的文件
3598
+ - search_value: str = "." 💡 搜索文本,可以是 sha1
3599
+ - show_dir: 0 | 1 = 1
3600
+ - source: str = <default>
3601
+ - star: 0 | 1 = <default>
3602
+ - suffix: str = <default>
3603
+ - type: int = <default> 💡 文件类型
3604
+
3605
+ - 0: 全部(仅当前目录)
3606
+ - 1: 文档
3607
+ - 2: 图片
3608
+ - 3: 音频
3609
+ - 4: 视频
3610
+ - 5: 压缩包
3611
+ - 6: 软件/应用
3612
+ - 7: 书籍
3613
+ - 99: 仅文件
3614
+
3615
+ - version: str = <default> 💡 版本号,比如 3.1
3616
+ """
3617
+ api = complete_proapi("/open/ufile/search", base_url)
3618
+ if isinstance(payload, str):
3619
+ payload = {
3620
+ "aid": 1, "cid": 0, "format": "json", "limit": 32, "offset": 0,
3621
+ "show_dir": 1, "search_value": payload,
3622
+ }
3623
+ else:
3624
+ payload = {
3625
+ "aid": 1, "cid": 0, "format": "json", "limit": 32, "offset": 0,
3626
+ "show_dir": 1, "search_value": ".", **payload,
3627
+ }
3628
+ return self.request(url=api, params=payload, async_=async_, **request_kwargs)
3629
+
3630
+ @overload
3631
+ def fs_star_set(
3632
+ self,
3633
+ payload: int | str | Iterable[int | str] | dict,
3634
+ /,
3635
+ star: bool = True,
3636
+ base_url: bool | str | Callable[[], str] = False,
3637
+ *,
3638
+ async_: Literal[False] = False,
3639
+ **request_kwargs,
3640
+ ) -> dict:
3641
+ ...
3642
+ @overload
3643
+ def fs_star_set(
3644
+ self,
3645
+ payload: int | str | Iterable[int | str] | dict,
3646
+ /,
3647
+ star: bool = True,
3648
+ base_url: bool | str | Callable[[], str] = False,
3649
+ *,
3650
+ async_: Literal[True],
3651
+ **request_kwargs,
3652
+ ) -> Coroutine[Any, Any, dict]:
3653
+ ...
3654
+ def fs_star_set(
3655
+ self,
3656
+ payload: int | str | Iterable[int | str] | dict,
3657
+ /,
3658
+ star: bool = True,
3659
+ base_url: bool | str | Callable[[], str] = False,
3660
+ *,
3661
+ async_: Literal[False, True] = False,
3662
+ **request_kwargs,
3663
+ ) -> dict | Coroutine[Any, Any, dict]:
3664
+ """为文件或目录设置或取消星标,此接口是对 `fs_update_open` 的封装
3665
+
3666
+ .. note::
3667
+ 即使其中任何一个 id 目前已经被删除,也可以操作成功
3668
+
3669
+ :payload:
3670
+ - file_id: int | str 💡 只能传入 1 个
3671
+ - file_id[0]: int | str 💡 如果有多个,则按顺序给出
3672
+ - file_id[1]: int | str
3673
+ - ...
3674
+ - star: 0 | 1 = 1
3675
+ """
3676
+ api = complete_webapi("/files/star", base_url=base_url)
3677
+ if isinstance(payload, (int, str)):
3678
+ payload = {"file_id": payload, "star": int(star)}
3679
+ elif not isinstance(payload, dict):
3680
+ payload = {f"file_id[{i}]": id for i, id in enumerate(payload)}
3681
+ if not payload:
3682
+ return {"state": False, "message": "no op"}
3683
+ payload["star"] = int(star)
3684
+ else:
3685
+ payload = {"star": int(star), **payload}
3686
+ return self.fs_update(payload, async_=async_, **request_kwargs)
3687
+
3688
+ @overload
3689
+ def fs_video_history(
3690
+ self,
3691
+ payload: str | dict,
3692
+ /,
3693
+ base_url: bool | str | Callable[[], str] = False,
3694
+ *,
3695
+ async_: Literal[False] = False,
3696
+ **request_kwargs,
3697
+ ) -> dict:
3698
+ ...
3699
+ @overload
3700
+ def fs_video_history(
3701
+ self,
3702
+ payload: str | dict,
3703
+ /,
3704
+ base_url: bool | str | Callable[[], str] = False,
3705
+ *,
3706
+ async_: Literal[True],
3707
+ **request_kwargs,
3708
+ ) -> Coroutine[Any, Any, dict]:
3709
+ ...
3710
+ def fs_video_history(
3711
+ self,
3712
+ payload: str | dict,
3713
+ /,
3714
+ base_url: bool | str | Callable[[], str] = False,
3715
+ *,
3716
+ async_: Literal[False, True] = False,
3717
+ **request_kwargs,
3718
+ ) -> dict | Coroutine[Any, Any, dict]:
3719
+ """获取视频播放进度
3720
+
3721
+ GET https://proapi.115.com/open/video/history
3722
+
3723
+ .. admonition:: Reference
3724
+
3725
+ https://www.yuque.com/115yun/open/gssqdrsq6vfqigag
3726
+
3727
+ :payload:
3728
+ - pick_code: str 💡 文件提取码
3729
+ """
3730
+ api = complete_proapi("/open/video/history", base_url)
3731
+ if isinstance(payload, str):
3732
+ payload = {"pick_code": payload}
3733
+ return self.request(url=api, params=payload, async_=async_, **request_kwargs)
3734
+
3735
+ @overload
3736
+ def fs_video_history_set(
3737
+ self,
3738
+ payload: str | dict,
3739
+ /,
3740
+ base_url: bool | str | Callable[[], str] = False,
3741
+ *,
3742
+ async_: Literal[False] = False,
3743
+ **request_kwargs,
3744
+ ) -> dict:
3745
+ ...
3746
+ @overload
3747
+ def fs_video_history_set(
3748
+ self,
3749
+ payload: str | dict,
3750
+ /,
3751
+ base_url: bool | str | Callable[[], str] = False,
3752
+ *,
3753
+ async_: Literal[True],
3754
+ **request_kwargs,
3755
+ ) -> Coroutine[Any, Any, dict]:
3756
+ ...
3757
+ def fs_video_history_set(
3758
+ self,
3759
+ payload: str | dict,
3760
+ /,
3761
+ base_url: bool | str | Callable[[], str] = False,
3762
+ *,
3763
+ async_: Literal[False, True] = False,
3764
+ **request_kwargs,
3765
+ ) -> dict | Coroutine[Any, Any, dict]:
3766
+ """记忆视频播放进度
3767
+
3768
+ POST https://proapi.115.com/open/video/history
3769
+
3770
+ .. admonition:: Reference
3771
+
3772
+ https://www.yuque.com/115yun/open/bshagbxv1gzqglg4
3773
+
3774
+ :payload:
3775
+ - pick_code: str 💡 文件提取码
3776
+ - time: int = <default> 💡 视频播放进度时长 (单位秒)
3777
+ - watch_end: int = <default> 💡 视频是否播放播放完毕 0:未完毕 1:完毕
3778
+ """
3779
+ api = complete_proapi("/open/video/history", base_url)
3780
+ if isinstance(payload, str):
3781
+ payload = {"pick_code": payload}
3782
+ return self.request(url=api, method="POST", data=payload, async_=async_, **request_kwargs)
3783
+
3784
+ @overload
3785
+ def fs_video_play(
3786
+ self,
3787
+ payload: str | dict,
3788
+ /,
3789
+ base_url: bool | str | Callable[[], str] = False,
3790
+ *,
3791
+ async_: Literal[False] = False,
3792
+ **request_kwargs,
3793
+ ) -> dict:
3794
+ ...
3795
+ @overload
3796
+ def fs_video_play(
3797
+ self,
3798
+ payload: str | dict,
3799
+ /,
3800
+ base_url: bool | str | Callable[[], str] = False,
3801
+ *,
3802
+ async_: Literal[True],
3803
+ **request_kwargs,
3804
+ ) -> Coroutine[Any, Any, dict]:
3805
+ ...
3806
+ def fs_video_play(
3807
+ self,
3808
+ payload: str | dict,
3809
+ /,
3810
+ base_url: bool | str | Callable[[], str] = False,
3811
+ *,
3812
+ async_: Literal[False, True] = False,
3813
+ **request_kwargs,
3814
+ ) -> dict | Coroutine[Any, Any, dict]:
3815
+ """获取视频在线播放地址(和视频文件相关数据)
3816
+
3817
+ GET https://proapi.115.com/open/video/play
3818
+
3819
+ .. admonition:: Reference
3820
+
3821
+ https://www.yuque.com/115yun/open/hqglxv3cedi3p9dz
3822
+
3823
+ .. hint::
3824
+ 需切换音轨时,在请求返回的播放地址中增加请求参数 `&audio_track=${index}`,值就是接口响应中 `multitrack_list` 中某个成员的索引,从 0 开始计数
3825
+
3826
+ :payload:
3827
+ - pick_code: str 💡 文件提取码
3828
+ - share_id: int | str = <default> 💡 共享 id,获取共享文件播放地址所需
3829
+ """
3830
+ api = complete_proapi("/open/video/play", base_url)
3831
+ if isinstance(payload, str):
3832
+ payload = {"pick_code": payload}
3833
+ return self.request(url=api, params=payload, async_=async_, **request_kwargs)
3834
+
3835
+ @overload
3836
+ def fs_video_push(
3837
+ self,
3838
+ payload: str | dict,
3839
+ /,
3840
+ base_url: bool | str | Callable[[], str] = False,
3841
+ *,
3842
+ async_: Literal[False] = False,
3843
+ **request_kwargs,
3844
+ ) -> dict:
3845
+ ...
3846
+ @overload
3847
+ def fs_video_push(
3848
+ self,
3849
+ payload: str | dict,
3850
+ /,
3851
+ base_url: bool | str | Callable[[], str] = False,
3852
+ *,
3853
+ async_: Literal[True],
3854
+ **request_kwargs,
3855
+ ) -> Coroutine[Any, Any, dict]:
3856
+ ...
3857
+ def fs_video_push(
3858
+ self,
3859
+ payload: str | dict,
3860
+ /,
3861
+ base_url: bool | str | Callable[[], str] = False,
3862
+ *,
3863
+ async_: Literal[False, True] = False,
3864
+ **request_kwargs,
3865
+ ) -> dict | Coroutine[Any, Any, dict]:
3866
+ """提交视频转码
3867
+
3868
+ POST https://proapi.115.com/open/video/video_push
3869
+
3870
+ .. admonition:: Reference
3871
+
3872
+ https://www.yuque.com/115yun/open/nxt8r1qcktmg3oan
3873
+
3874
+ :payload:
3875
+ - pick_code: str 💡 文件提取码
3876
+ - op: str = "vip_push" 💡 提交视频加速转码方式:vip_push:根据;vip 等级加速 pay_push:枫叶加速
3877
+ """
3878
+ api = complete_proapi("/open/video/video_push", base_url)
3879
+ if isinstance(payload, str):
3880
+ payload = {"pick_code": payload, "op": "vip_push"}
3881
+ else:
3882
+ payload.setdefault("op", "vip_push")
3883
+ return self.request(url=api, method="POST", data=payload, async_=async_, **request_kwargs)
3884
+
3885
+ @overload
3886
+ def fs_video_subtitle(
3887
+ self,
3888
+ payload: str | dict,
3889
+ /,
3890
+ base_url: bool | str | Callable[[], str] = False,
3891
+ *,
3892
+ async_: Literal[False] = False,
3893
+ **request_kwargs,
3894
+ ) -> dict:
3895
+ ...
3896
+ @overload
3897
+ def fs_video_subtitle(
3898
+ self,
3899
+ payload: str | dict,
3900
+ /,
3901
+ base_url: bool | str | Callable[[], str] = False,
3902
+ *,
3903
+ async_: Literal[True],
3904
+ **request_kwargs,
3905
+ ) -> Coroutine[Any, Any, dict]:
3906
+ ...
3907
+ def fs_video_subtitle(
3908
+ self,
3909
+ payload: str | dict,
3910
+ /,
3911
+ base_url: bool | str | Callable[[], str] = False,
3912
+ *,
3913
+ async_: Literal[False, True] = False,
3914
+ **request_kwargs,
3915
+ ) -> dict | Coroutine[Any, Any, dict]:
3916
+ """视频字幕列表
3917
+
3918
+ GET https://proapi.115.com/open/video/subtitle
3919
+
3920
+ .. admonition:: Reference
3921
+
3922
+ https://www.yuque.com/115yun/open/nx076h3glapoyh7u
3923
+
3924
+ :payload:
3925
+ - pick_code: str 💡 文件提取码
3926
+ """
3927
+ api = complete_proapi("/open/video/subtitle", base_url)
3928
+ if isinstance(payload, str):
3929
+ payload = {"pick_code": payload}
3930
+ return self.request(url=api, params=payload, async_=async_, **request_kwargs)
3931
+
3932
+ @overload
3933
+ def fs_update(
3934
+ self,
3935
+ payload: dict,
3936
+ /,
3937
+ base_url: bool | str | Callable[[], str] = False,
3938
+ *,
3939
+ async_: Literal[False] = False,
3940
+ **request_kwargs,
3941
+ ) -> dict:
3942
+ ...
3943
+ @overload
3944
+ def fs_update(
3945
+ self,
3946
+ payload: dict,
3947
+ /,
3948
+ base_url: bool | str | Callable[[], str] = False,
3949
+ *,
3950
+ async_: Literal[True],
3951
+ **request_kwargs,
3952
+ ) -> Coroutine[Any, Any, dict]:
3953
+ ...
3954
+ def fs_update(
3955
+ self,
3956
+ payload: dict,
3957
+ /,
3958
+ base_url: bool | str | Callable[[], str] = False,
3959
+ *,
3960
+ async_: Literal[False, True] = False,
3961
+ **request_kwargs,
3962
+ ) -> dict | Coroutine[Any, Any, dict]:
3963
+ """设置文件或目录(备注、标签等)
3964
+
3965
+ POST https://proapi.115.com/open/ufile/update
3966
+
3967
+ .. hint::
3968
+ 即使文件已经被删除,也可以操作成功
3969
+
3970
+ .. admonition:: Reference
3971
+
3972
+ https://www.yuque.com/115yun/open/gyrpw5a0zc4sengm
3973
+
3974
+ :payload:
3975
+ - file_id: int | str 💡 只能传入 1 个
3976
+ - file_id[0]: int | str 💡 如果有多个,则按顺序给出
3977
+ - file_id[1]: int | str
3978
+ - ...
3979
+ - file_name: str = <default> 💡 文件名
3980
+ - star: 0 | 1 = <default> 💡 是否星标:0:取消星标 1:设置星标
3981
+ - ...
3982
+ """
3983
+ api = complete_proapi("/open/ufile/update", base_url)
3984
+ return self.request(url=api, method="POST", data=payload, async_=async_, **request_kwargs)
3985
+
3986
+ @overload
3987
+ def offline_add_torrent(
3988
+ self,
3989
+ payload: dict,
3990
+ /,
3991
+ base_url: bool | str | Callable[[], str] = False,
3992
+ *,
3993
+ async_: Literal[False] = False,
3994
+ **request_kwargs,
3995
+ ) -> dict:
3996
+ ...
3997
+ @overload
3998
+ def offline_add_torrent(
3999
+ self,
4000
+ payload: dict,
4001
+ /,
4002
+ base_url: bool | str | Callable[[], str] = False,
4003
+ *,
4004
+ async_: Literal[True],
4005
+ **request_kwargs,
4006
+ ) -> Coroutine[Any, Any, dict]:
4007
+ ...
4008
+ def offline_add_torrent(
4009
+ self,
4010
+ payload: dict,
4011
+ /,
4012
+ base_url: bool | str | Callable[[], str] = False,
4013
+ *,
4014
+ async_: Literal[False, True] = False,
4015
+ **request_kwargs,
4016
+ ) -> dict | Coroutine[Any, Any, dict]:
4017
+ """添加云下载 BT 任务
4018
+
4019
+ POST https://proapi.115.com/open/offline/add_task_bt
4020
+
4021
+ .. admonition:: Reference
4022
+
4023
+ https://www.yuque.com/115yun/open/svfe4unlhayvluly
4024
+
4025
+ :payload:
4026
+ - info_hash: str 💡 种子文件的 info_hash
4027
+ - pick_code: str 💡 种子文件的提取码
4028
+ - save_path: str 💡 保存到 `wp_path_id` 对应目录下的相对路径
4029
+ - torrent_sha1: str 💡 种子文件的 sha1
4030
+ - wanted: str 💡 选择文件进行下载(是数字索引,从 0 开始计数,用 "," 分隔)
4031
+ - wp_path_id: int | str = <default> 💡 保存目标文件夹 id
4032
+ """
4033
+ api = complete_proapi("/open/offline/add_task_bt ", base_url)
4034
+ return self.request(url=api, method="POST", data=payload, async_=async_, **request_kwargs)
4035
+
4036
+ @overload
4037
+ def offline_add_urls(
3140
4038
  self,
3141
- payload: int | str | dict,
4039
+ payload: str | Iterable[str] | dict,
3142
4040
  /,
3143
4041
  base_url: bool | str | Callable[[], str] = False,
3144
4042
  *,
@@ -3147,9 +4045,9 @@ class P115OpenClient(ClientRequestMixin):
3147
4045
  ) -> dict:
3148
4046
  ...
3149
4047
  @overload
3150
- def fs_info(
4048
+ def offline_add_urls(
3151
4049
  self,
3152
- payload: int | str | dict,
4050
+ payload: str | Iterable[str] | dict,
3153
4051
  /,
3154
4052
  base_url: bool | str | Callable[[], str] = False,
3155
4053
  *,
@@ -3157,40 +4055,39 @@ class P115OpenClient(ClientRequestMixin):
3157
4055
  **request_kwargs,
3158
4056
  ) -> Coroutine[Any, Any, dict]:
3159
4057
  ...
3160
- def fs_info(
4058
+ def offline_add_urls(
3161
4059
  self,
3162
- payload: int | str | dict,
4060
+ payload: str | Iterable[str] | dict,
3163
4061
  /,
3164
4062
  base_url: bool | str | Callable[[], str] = False,
3165
4063
  *,
3166
4064
  async_: Literal[False, True] = False,
3167
4065
  **request_kwargs,
3168
4066
  ) -> dict | Coroutine[Any, Any, dict]:
3169
- """获取文件或目录详情
4067
+ """添加云下载链接任务
3170
4068
 
3171
- GET https://proapi.115.com/open/folder/get_info
3172
-
3173
- .. hint::
3174
- 相当于 `P115Client.fs_category_get_app`
4069
+ POST https://proapi.115.com/open/offline/add_task_urls
3175
4070
 
3176
4071
  .. admonition:: Reference
3177
4072
 
3178
- https://www.yuque.com/115yun/open/rl8zrhe2nag21dfw
4073
+ https://www.yuque.com/115yun/open/zkyfq2499gdn3mty
3179
4074
 
3180
4075
  :payload:
3181
- - file_id: int | str 💡 文件或目录的 id
4076
+ - urls: str 💡 链接,用 "\\n" 分隔,支持HTTP、HTTPS、FTP、磁力链和电驴链接
4077
+ - wp_path_id: int | str = <default> 💡 保存到目录的 id
3182
4078
  """
3183
- api = complete_proapi("/open/folder/get_info", base_url)
3184
- if isinstance(payload, (int, str)):
3185
- payload = {"file_id": payload}
3186
- return self.request(url=api, params=payload, async_=async_, **request_kwargs)
4079
+ api = complete_proapi("/open/offline/add_task_urls", base_url)
4080
+ if isinstance(payload, str):
4081
+ payload = {"urls": payload.strip("\n")}
4082
+ elif not isinstance(payload, dict):
4083
+ payload = {"urls": ",".join(payload)}
4084
+ return self.request(url=api, method="POST", data=payload, async_=async_, **request_kwargs)
3187
4085
 
3188
4086
  @overload
3189
- def fs_mkdir(
4087
+ def offline_clear(
3190
4088
  self,
3191
- payload: str | dict,
4089
+ payload: int | dict = 0,
3192
4090
  /,
3193
- pid: int = 0,
3194
4091
  base_url: bool | str | Callable[[], str] = False,
3195
4092
  *,
3196
4093
  async_: Literal[False] = False,
@@ -3198,52 +4095,53 @@ class P115OpenClient(ClientRequestMixin):
3198
4095
  ) -> dict:
3199
4096
  ...
3200
4097
  @overload
3201
- def fs_mkdir(
4098
+ def offline_clear(
3202
4099
  self,
3203
- payload: str | dict,
4100
+ payload: int | dict = 0,
3204
4101
  /,
3205
- pid: int = 0,
3206
4102
  base_url: bool | str | Callable[[], str] = False,
3207
4103
  *,
3208
4104
  async_: Literal[True],
3209
4105
  **request_kwargs,
3210
4106
  ) -> Coroutine[Any, Any, dict]:
3211
4107
  ...
3212
- def fs_mkdir(
4108
+ def offline_clear(
3213
4109
  self,
3214
- payload: str | dict,
4110
+ payload: int | dict = 0,
3215
4111
  /,
3216
- pid: int = 0,
3217
4112
  base_url: bool | str | Callable[[], str] = False,
3218
4113
  *,
3219
4114
  async_: Literal[False, True] = False,
3220
4115
  **request_kwargs,
3221
4116
  ) -> dict | Coroutine[Any, Any, dict]:
3222
- """新建目录
4117
+ """清空云下载任务
3223
4118
 
3224
- POST https://proapi.115.com/open/folder/add
4119
+ POST https://proapi.115.com/open/offline/clear_task
3225
4120
 
3226
4121
  .. admonition:: Reference
3227
4122
 
3228
- https://www.yuque.com/115yun/open/qur839kyx9cgxpxi
4123
+ https://www.yuque.com/115yun/open/uu5i4urb5ylqwfy4
3229
4124
 
3230
4125
  :payload:
3231
- - file_name: str 💡 新建目录名称,限制255个字符
3232
- - pid: int | str = 0 💡 新建目录所在的父目录ID (根目录的ID为0)
4126
+ - flag: int = 0 💡 标识,用于对应某种情况
4127
+
4128
+ - 0: 已完成
4129
+ - 1: 全部
4130
+ - 2: 已失败
4131
+ - 3: 进行中
4132
+ - 4: 已完成+删除源文件
4133
+ - 5: 全部+删除源文件
3233
4134
  """
3234
- api = complete_proapi("/open/folder/add", base_url)
3235
- if isinstance(payload, str):
3236
- payload = {"pid": pid, "file_name": payload}
3237
- else:
3238
- payload = {"pid": pid, **payload}
4135
+ api = complete_proapi("/open/offline/clear_task", base_url)
4136
+ if isinstance(payload, int):
4137
+ payload = {"flag": payload}
3239
4138
  return self.request(url=api, method="POST", data=payload, async_=async_, **request_kwargs)
3240
4139
 
3241
4140
  @overload
3242
- def fs_move(
4141
+ def offline_list(
3243
4142
  self,
3244
- payload: int | str | Iterable[int | str] | dict,
4143
+ payload: int | dict = 1,
3245
4144
  /,
3246
- pid: int = 0,
3247
4145
  base_url: bool | str | Callable[[], str] = False,
3248
4146
  *,
3249
4147
  async_: Literal[False] = False,
@@ -3251,56 +4149,44 @@ class P115OpenClient(ClientRequestMixin):
3251
4149
  ) -> dict:
3252
4150
  ...
3253
4151
  @overload
3254
- def fs_move(
4152
+ def offline_list(
3255
4153
  self,
3256
- payload: int | str | Iterable[int | str] | dict,
4154
+ payload: int | dict = 1,
3257
4155
  /,
3258
- pid: int = 0,
3259
4156
  base_url: bool | str | Callable[[], str] = False,
3260
4157
  *,
3261
4158
  async_: Literal[True],
3262
4159
  **request_kwargs,
3263
4160
  ) -> Coroutine[Any, Any, dict]:
3264
4161
  ...
3265
- def fs_move(
4162
+ def offline_list(
3266
4163
  self,
3267
- payload: int | str | Iterable[int | str] | dict,
4164
+ payload: int | dict = 1,
3268
4165
  /,
3269
- pid: int = 0,
3270
4166
  base_url: bool | str | Callable[[], str] = False,
3271
4167
  *,
3272
4168
  async_: Literal[False, True] = False,
3273
4169
  **request_kwargs,
3274
4170
  ) -> dict | Coroutine[Any, Any, dict]:
3275
- """文件移动
4171
+ """获取用户云下载任务列表
3276
4172
 
3277
- POST https://proapi.115.com/open/ufile/move
4173
+ GET https://proapi.115.com/open/offline/get_task_list
3278
4174
 
3279
4175
  .. admonition:: Reference
3280
4176
 
3281
- https://www.yuque.com/115yun/open/vc6fhi2mrkenmav2
4177
+ https://www.yuque.com/115yun/open/av2mluz7uwigz74k
3282
4178
 
3283
4179
  :payload:
3284
- - file_ids: int | str 💡 文件或目录的 id,多个用逗号 "," 隔开
3285
- - to_cid: int | str = 0 💡 父目录 id
4180
+ - page: int | str = 1
3286
4181
  """
3287
- api = complete_proapi("/open/ufile/move", base_url)
3288
- if isinstance(payload, (int, str)):
3289
- payload = {"file_ids": payload}
3290
- elif isinstance(payload, dict):
3291
- payload = dict(payload)
3292
- else:
3293
- payload = {"file_ids": ",".join(map(str, payload))}
3294
- if not payload.get("file_ids"):
3295
- return {"state": False, "message": "no op"}
3296
- payload = cast(dict, payload)
3297
- payload.setdefault("to_cid", pid)
3298
- return self.request(url=api, method="POST", data=payload, async_=async_, **request_kwargs)
4182
+ api = complete_proapi("/open/offline/get_task_list", base_url)
4183
+ if isinstance(payload, int):
4184
+ payload = {"page": payload}
4185
+ return self.request(url=api, params=payload, async_=async_, **request_kwargs)
3299
4186
 
3300
4187
  @overload
3301
- def fs_search(
4188
+ def offline_quota_info(
3302
4189
  self,
3303
- payload: str | dict = ".",
3304
4190
  /,
3305
4191
  base_url: bool | str | Callable[[], str] = False,
3306
4192
  *,
@@ -3309,9 +4195,8 @@ class P115OpenClient(ClientRequestMixin):
3309
4195
  ) -> dict:
3310
4196
  ...
3311
4197
  @overload
3312
- def fs_search(
4198
+ def offline_quota_info(
3313
4199
  self,
3314
- payload: str | dict = ".",
3315
4200
  /,
3316
4201
  base_url: bool | str | Callable[[], str] = False,
3317
4202
  *,
@@ -3319,88 +4204,30 @@ class P115OpenClient(ClientRequestMixin):
3319
4204
  **request_kwargs,
3320
4205
  ) -> Coroutine[Any, Any, dict]:
3321
4206
  ...
3322
- def fs_search(
4207
+ def offline_quota_info(
3323
4208
  self,
3324
- payload: str | dict = ".",
3325
4209
  /,
3326
4210
  base_url: bool | str | Callable[[], str] = False,
3327
4211
  *,
3328
4212
  async_: Literal[False, True] = False,
3329
4213
  **request_kwargs,
3330
4214
  ) -> dict | Coroutine[Any, Any, dict]:
3331
- """搜索文件或目录
3332
-
3333
- GET https://proapi.115.com/open/ufile/search
4215
+ """获取云下载配额信息
3334
4216
 
3335
- .. hint::
3336
- 相当于 `P115Client.fs_search_app2`
4217
+ GET https://proapi.115.com/open/offline/get_quota_info
3337
4218
 
3338
4219
  .. admonition:: Reference
3339
4220
 
3340
- https://www.yuque.com/115yun/open/ft2yelxzopusus38
3341
-
3342
- :payload:
3343
- - aid: int | str = 1 💡 area_id。1:正常文件 7:回收站文件 12:瞬间文件 120:彻底删除文件、简历附件
3344
- - asc: 0 | 1 = <default> 💡 是否升序排列
3345
- - cid: int | str = 0 💡 目录 id。cid=-1 时,表示不返回列表任何内容
3346
- - count_folders: 0 | 1 = <default>
3347
- - date: str = <default> 💡 筛选日期
3348
- - fc: 0 | 1 = <default> 💡 只显示文件或目录。1:只显示目录 2:只显示文件
3349
- - fc_mix: 0 | 1 = <default> 💡 是否目录和文件混合,如果为 0 则目录在前(目录置顶)
3350
- - file_label: int | str = <default> 💡 标签 id
3351
- - format: str = "json" 💡 输出格式(不用管)
3352
- - gte_day: str 💡 搜索结果匹配的开始时间;格式:YYYY-MM-DD
3353
- - limit: int = 32 💡 一页大小,意思就是 page_size
3354
- - lte_day: str 💡 搜索结果匹配的结束时间;格式:YYYY-MM-DD
3355
- - o: str = <default> 💡 用某字段排序
3356
-
3357
- - "file_name": 文件名
3358
- - "file_size": 文件大小
3359
- - "file_type": 文件种类
3360
- - "user_utime": 修改时间
3361
- - "user_ptime": 创建时间
3362
- - "user_otime": 上一次打开时间
3363
-
3364
- - offset: int = 0 💡 索引偏移,索引从 0 开始计算
3365
- - pick_code: str = <default> 💡 是否查询提取码,如果该值为 1 则查询提取码为 `search_value` 的文件
3366
- - search_value: str = "." 💡 搜索文本,可以是 sha1
3367
- - show_dir: 0 | 1 = 1
3368
- - source: str = <default>
3369
- - star: 0 | 1 = <default>
3370
- - suffix: str = <default>
3371
- - type: int = <default> 💡 文件类型
3372
-
3373
- - 0: 全部(仅当前目录)
3374
- - 1: 文档
3375
- - 2: 图片
3376
- - 3: 音频
3377
- - 4: 视频
3378
- - 5: 压缩包
3379
- - 6: 软件/应用
3380
- - 7: 书籍
3381
- - 99: 仅文件
3382
-
3383
- - version: str = <default> 💡 版本号,比如 3.1
4221
+ https://www.yuque.com/115yun/open/gif2n3smh54kyg0p
3384
4222
  """
3385
- api = complete_proapi("/open/ufile/search", base_url)
3386
- if isinstance(payload, str):
3387
- payload = {
3388
- "aid": 1, "cid": 0, "format": "json", "limit": 32, "offset": 0,
3389
- "show_dir": 1, "search_value": payload,
3390
- }
3391
- else:
3392
- payload = {
3393
- "aid": 1, "cid": 0, "format": "json", "limit": 32, "offset": 0,
3394
- "show_dir": 1, "search_value": ".", **payload,
3395
- }
3396
- return self.request(url=api, params=payload, async_=async_, **request_kwargs)
4223
+ api = complete_proapi("/open/offline/get_quota_info", base_url)
4224
+ return self.request(url=api, async_=async_, **request_kwargs)
3397
4225
 
3398
4226
  @overload
3399
- def fs_star_set(
4227
+ def offline_remove(
3400
4228
  self,
3401
- payload: int | str | Iterable[int | str] | dict,
4229
+ payload: str | dict,
3402
4230
  /,
3403
- star: bool = True,
3404
4231
  base_url: bool | str | Callable[[], str] = False,
3405
4232
  *,
3406
4233
  async_: Literal[False] = False,
@@ -3408,53 +4235,44 @@ class P115OpenClient(ClientRequestMixin):
3408
4235
  ) -> dict:
3409
4236
  ...
3410
4237
  @overload
3411
- def fs_star_set(
4238
+ def offline_remove(
3412
4239
  self,
3413
- payload: int | str | Iterable[int | str] | dict,
4240
+ payload: str | dict,
3414
4241
  /,
3415
- star: bool = True,
3416
4242
  base_url: bool | str | Callable[[], str] = False,
3417
4243
  *,
3418
4244
  async_: Literal[True],
3419
4245
  **request_kwargs,
3420
4246
  ) -> Coroutine[Any, Any, dict]:
3421
4247
  ...
3422
- def fs_star_set(
4248
+ def offline_remove(
3423
4249
  self,
3424
- payload: int | str | Iterable[int | str] | dict,
4250
+ payload: str | dict,
3425
4251
  /,
3426
- star: bool = True,
3427
4252
  base_url: bool | str | Callable[[], str] = False,
3428
4253
  *,
3429
4254
  async_: Literal[False, True] = False,
3430
4255
  **request_kwargs,
3431
4256
  ) -> dict | Coroutine[Any, Any, dict]:
3432
- """为文件或目录设置或取消星标,此接口是对 `fs_update_open` 的封装
4257
+ """删除用户云下载任务
3433
4258
 
3434
- .. note::
3435
- 即使其中任何一个 id 目前已经被删除,也可以操作成功
4259
+ POST https://proapi.115.com/open/offline/del_task
4260
+
4261
+ .. admonition:: Reference
4262
+
4263
+ https://www.yuque.com/115yun/open/pmgwc86lpcy238nw
3436
4264
 
3437
4265
  :payload:
3438
- - file_id: int | str 💡 只能传入 1 个
3439
- - file_id[0]: int | str 💡 如果有多个,则按顺序给出
3440
- - file_id[1]: int | str
3441
- - ...
3442
- - star: 0 | 1 = 1
4266
+ - info_hash: str 💡 待删除任务的 info_hash
4267
+ - del_source_file: 0 | 1 = <default> 💡 是否删除源文件 1:删除 0:不删除
3443
4268
  """
3444
- api = complete_webapi("/files/star", base_url=base_url)
3445
- if isinstance(payload, (int, str)):
3446
- payload = {"file_id": payload, "star": int(star)}
3447
- elif not isinstance(payload, dict):
3448
- payload = {f"file_id[{i}]": id for i, id in enumerate(payload)}
3449
- if not payload:
3450
- return {"state": False, "message": "no op"}
3451
- payload["star"] = int(star)
3452
- else:
3453
- payload = {"star": int(star), **payload}
3454
- return self.fs_update(payload, async_=async_, **request_kwargs)
4269
+ api = complete_proapi("/open/offline/del_task", base_url)
4270
+ if isinstance(payload, str):
4271
+ payload = {"info_hash": payload}
4272
+ return self.request(api, method="POST", data=payload, async_=async_, **request_kwargs)
3455
4273
 
3456
4274
  @overload
3457
- def fs_update(
4275
+ def offline_torrent_info(
3458
4276
  self,
3459
4277
  payload: dict,
3460
4278
  /,
@@ -3465,7 +4283,7 @@ class P115OpenClient(ClientRequestMixin):
3465
4283
  ) -> dict:
3466
4284
  ...
3467
4285
  @overload
3468
- def fs_update(
4286
+ def offline_torrent_info(
3469
4287
  self,
3470
4288
  payload: dict,
3471
4289
  /,
@@ -3475,7 +4293,7 @@ class P115OpenClient(ClientRequestMixin):
3475
4293
  **request_kwargs,
3476
4294
  ) -> Coroutine[Any, Any, dict]:
3477
4295
  ...
3478
- def fs_update(
4296
+ def offline_torrent_info(
3479
4297
  self,
3480
4298
  payload: dict,
3481
4299
  /,
@@ -3484,27 +4302,19 @@ class P115OpenClient(ClientRequestMixin):
3484
4302
  async_: Literal[False, True] = False,
3485
4303
  **request_kwargs,
3486
4304
  ) -> dict | Coroutine[Any, Any, dict]:
3487
- """设置文件或目录(备注、标签等)
3488
-
3489
- POST https://proapi.115.com/open/ufile/update
4305
+ """解析 BT 种子
3490
4306
 
3491
- .. hint::
3492
- 即使文件已经被删除,也可以操作成功
4307
+ POST https://proapi.115.com/open/offline/torrent
3493
4308
 
3494
4309
  .. admonition:: Reference
3495
4310
 
3496
- https://www.yuque.com/115yun/open/gyrpw5a0zc4sengm
4311
+ https://www.yuque.com/115yun/open/evez3u50cemoict1
3497
4312
 
3498
4313
  :payload:
3499
- - file_id: int | str 💡 只能传入 1 个
3500
- - file_id[0]: int | str 💡 如果有多个,则按顺序给出
3501
- - file_id[1]: int | str
3502
- - ...
3503
- - file_name: str = <default> 💡 文件名
3504
- - star: 0 | 1 = <default> 💡 是否星标:0:取消星标 1:设置星标
3505
- - ...
4314
+ - torrent_sha1: str 💡 种子文件的 sha1
4315
+ - pick_code: str 💡 种子文件的提取码
3506
4316
  """
3507
- api = complete_proapi("/open/ufile/update", base_url)
4317
+ api = complete_proapi("/open/offline/torrent", base_url)
3508
4318
  return self.request(url=api, method="POST", data=payload, async_=async_, **request_kwargs)
3509
4319
 
3510
4320
  @overload
@@ -3542,7 +4352,8 @@ class P115OpenClient(ClientRequestMixin):
3542
4352
 
3543
4353
  POST https://proapi.115.com/open/rb/del
3544
4354
 
3545
- .. note:
4355
+ .. admonition:: Reference
4356
+
3546
4357
  https://www.yuque.com/115yun/open/gwtof85nmboulrce
3547
4358
 
3548
4359
  :payload:
@@ -3900,7 +4711,7 @@ class P115OpenClient(ClientRequestMixin):
3900
4711
  check_response(resp)
3901
4712
  resp["data"] = {**payload, **resp["data"], "sha1": filesha1, "cid": pid}
3902
4713
  return resp
3903
- return run_gen_step(gen_step, async_=async_)
4714
+ return run_gen_step(gen_step, simple=True, async_=async_)
3904
4715
 
3905
4716
  @overload
3906
4717
  def upload_file(
@@ -4244,7 +5055,7 @@ class P115OpenClient(ClientRequestMixin):
4244
5055
  async_=async_, # type: ignore
4245
5056
  **request_kwargs,
4246
5057
  )
4247
- return run_gen_step(gen_step, async_=async_)
5058
+ return run_gen_step(gen_step, simple=True, async_=async_)
4248
5059
 
4249
5060
  @overload
4250
5061
  def user_info(
@@ -4295,7 +5106,19 @@ class P115OpenClient(ClientRequestMixin):
4295
5106
  fs_move_open = fs_move
4296
5107
  fs_search_open = fs_search
4297
5108
  fs_star_set_open = fs_star_set
5109
+ fs_video_history_open = fs_video_history
5110
+ fs_video_history_set_open = fs_video_history_set
5111
+ fs_video_play_open = fs_video_play
5112
+ fs_video_push_open = fs_video_push
5113
+ fs_video_subtitle_open = fs_video_subtitle
4298
5114
  fs_update_open = fs_update
5115
+ offline_add_torrent_open = offline_add_torrent
5116
+ offline_add_urls_open = offline_add_urls
5117
+ offline_clear_open = offline_clear
5118
+ offline_list_open = offline_list
5119
+ offline_quota_info_open = offline_quota_info
5120
+ offline_remove_open = offline_remove
5121
+ offline_torrent_info_open = offline_torrent_info
4299
5122
  recyclebin_clean_open = recyclebin_clean
4300
5123
  recyclebin_list_open = recyclebin_list
4301
5124
  recyclebin_revert_open = recyclebin_revert
@@ -4489,10 +5312,6 @@ class P115Client(P115OpenClient):
4489
5312
  """
4490
5313
  self.cookies = None
4491
5314
 
4492
- @locked_cacheproperty
4493
- def request_kwargs(self, /) -> dict:
4494
- return {}
4495
-
4496
5315
  @locked_cacheproperty
4497
5316
  def user_id(self, /) -> int:
4498
5317
  cookie_uid = self.cookies.get("UID")
@@ -4617,7 +5436,7 @@ class P115Client(P115OpenClient):
4617
5436
  )
4618
5437
  setattr(self, "check_for_relogin", check_for_relogin)
4619
5438
  return self
4620
- return run_gen_step(gen_step, async_=async_)
5439
+ return run_gen_step(gen_step, simple=True, async_=async_)
4621
5440
 
4622
5441
  @locked_cacheproperty
4623
5442
  def request_lock(self, /) -> Lock:
@@ -4789,7 +5608,7 @@ class P115Client(P115OpenClient):
4789
5608
  check_response(resp)
4790
5609
  setattr(self, "cookies", resp["data"]["cookie"])
4791
5610
  return self
4792
- return run_gen_step(gen_step, async_=async_)
5611
+ return run_gen_step(gen_step, simple=True, async_=async_)
4793
5612
 
4794
5613
  @overload
4795
5614
  def login_with_app(
@@ -4921,7 +5740,7 @@ class P115Client(P115OpenClient):
4921
5740
  async_=async_,
4922
5741
  **request_kwargs,
4923
5742
  )
4924
- return run_gen_step(gen_step, async_=async_)
5743
+ return run_gen_step(gen_step, simple=True, async_=async_)
4925
5744
 
4926
5745
  @overload
4927
5746
  def login_without_app(
@@ -4979,7 +5798,7 @@ class P115Client(P115OpenClient):
4979
5798
  )
4980
5799
  check_response(resp)
4981
5800
  return uid
4982
- return run_gen_step(gen_step, async_=async_)
5801
+ return run_gen_step(gen_step, simple=True, async_=async_)
4983
5802
 
4984
5803
  @overload
4985
5804
  def login_with_open(
@@ -5031,7 +5850,7 @@ class P115Client(P115OpenClient):
5031
5850
  resp = yield self.login_qrcode_scan_confirm(login_uid, async_=async_, **request_kwargs)
5032
5851
  check_response(resp)
5033
5852
  return self.login_qrcode_access_token_open(login_uid, async_=async_, **request_kwargs)
5034
- return run_gen_step(gen_step, async_=async_)
5853
+ return run_gen_step(gen_step, simple=True, async_=async_)
5035
5854
 
5036
5855
  @overload
5037
5856
  def login_another_app(
@@ -5175,7 +5994,7 @@ class P115Client(P115OpenClient):
5175
5994
  if self is not inst and ssoent == inst.login_ssoent:
5176
5995
  warn(f"login with the same ssoent {ssoent!r}, {self!r} will expire within 60 seconds", category=P115Warning)
5177
5996
  return inst
5178
- return run_gen_step(gen_step, async_=async_)
5997
+ return run_gen_step(gen_step, simple=True, async_=async_)
5179
5998
 
5180
5999
  @overload
5181
6000
  def login_another_open(
@@ -5269,7 +6088,7 @@ class P115Client(P115OpenClient):
5269
6088
  inst.access_token = data["access_token"]
5270
6089
  inst.app_id = app_id
5271
6090
  return inst
5272
- return run_gen_step(gen_step, async_=async_)
6091
+ return run_gen_step(gen_step, simple=True, async_=async_)
5273
6092
 
5274
6093
  @overload
5275
6094
  @classmethod
@@ -5388,7 +6207,7 @@ class P115Client(P115OpenClient):
5388
6207
  resp = yield cls.login_qrcode_scan_result(uid, app, async_=async_, **request_kwargs)
5389
6208
  cookies = check_response(resp)["data"]["cookie"]
5390
6209
  return cls(cookies, check_for_relogin=check_for_relogin)
5391
- return run_gen_step(gen_step, async_=async_)
6210
+ return run_gen_step(gen_step, simple=True, async_=async_)
5392
6211
 
5393
6212
  @overload
5394
6213
  def logout(
@@ -5540,7 +6359,7 @@ class P115Client(P115OpenClient):
5540
6359
  if async_:
5541
6360
  fetch_cert_headers = ensure_async(fetch_cert_headers)
5542
6361
  if fetch_cert_headers_argcount:
5543
- fetch_cert_headers = partial(fetch_cert_headers, async_)
6362
+ fetch_cert_headers = cast(Callable, fetch_cert_headers)(async_)
5544
6363
  if revert_cert_headers is not None and async_:
5545
6364
  revert_cert_headers = ensure_async(revert_cert_headers)
5546
6365
  if is_open_api:
@@ -5594,8 +6413,7 @@ class P115Client(P115OpenClient):
5594
6413
  cert: str = headers["authorization"]
5595
6414
  else:
5596
6415
  cert = headers["cookie"]
5597
- resp = yield partial(
5598
- cast(Callable, request),
6416
+ resp = yield cast(Callable, request)(
5599
6417
  url=url,
5600
6418
  method=method,
5601
6419
  **request_kwargs,
@@ -5608,10 +6426,10 @@ class P115Client(P115OpenClient):
5608
6426
  not is_auth_error and
5609
6427
  get_status_code(e) != 405
5610
6428
  ):
5611
- yield partial(revert_cert_headers, cert_headers)
6429
+ yield revert_cert_headers(cert_headers)
5612
6430
  if not need_to_check:
5613
6431
  raise
5614
- res = yield partial(cast(Callable, check_for_relogin), e)
6432
+ res = yield cast(Callable, check_for_relogin)(e)
5615
6433
  if not res if isinstance(res, bool) else res != 405:
5616
6434
  raise
5617
6435
  if fetch_cert_headers is not None:
@@ -5660,11 +6478,11 @@ class P115Client(P115OpenClient):
5660
6478
  lock.release()
5661
6479
  else:
5662
6480
  if cert_headers is not None and revert_cert_headers is not None:
5663
- yield partial(revert_cert_headers, cert_headers)
6481
+ yield revert_cert_headers(cert_headers)
5664
6482
  if check and isinstance(resp, dict):
5665
6483
  check_response(resp)
5666
6484
  return resp
5667
- return run_gen_step(gen_step, async_=async_)
6485
+ return run_gen_step(gen_step, simple=True, async_=async_)
5668
6486
 
5669
6487
  def request(
5670
6488
  self,
@@ -6516,15 +7334,14 @@ class P115Client(P115OpenClient):
6516
7334
  if "sign" not in payload:
6517
7335
  resp = yield self.captcha_sign(async_=async_)
6518
7336
  payload["sign"] = resp["sign"]
6519
- return partial(
6520
- self.request,
7337
+ return self.request(
6521
7338
  url=api,
6522
7339
  method="POST",
6523
7340
  data=payload,
6524
7341
  async_=async_,
6525
7342
  **request_kwargs,
6526
7343
  )
6527
- return run_gen_step(gen_step, async_=async_)
7344
+ return run_gen_step(gen_step, simple=True, async_=async_)
6528
7345
 
6529
7346
  ########## Download API ##########
6530
7347
 
@@ -15099,7 +15916,7 @@ class P115Client(P115OpenClient):
15099
15916
  if device is None:
15100
15917
  return None
15101
15918
  return device["icon"]
15102
- return run_gen_step(gen_step, async_=async_)
15919
+ return run_gen_step(gen_step, simple=True, async_=async_)
15103
15920
 
15104
15921
  @overload
15105
15922
  def login_open_auth_detail(
@@ -15623,7 +16440,7 @@ class P115Client(P115OpenClient):
15623
16440
  return get_default_request()(url=api, async_=async_, **request_kwargs)
15624
16441
  else:
15625
16442
  return request(url=api, **request_kwargs)
15626
- return run_gen_step(gen_step, async_=async_)
16443
+ return run_gen_step(gen_step, simple=True, async_=async_)
15627
16444
 
15628
16445
  @overload
15629
16446
  def logout_by_ssoent(
@@ -16716,7 +17533,7 @@ class P115Client(P115OpenClient):
16716
17533
  method = self._offline_lixianssp_post
16717
17534
  return method(payload, ac, async_=async_, ecdh_encrypt=ecdh_encrypt, **request_kwargs)
16718
17535
 
16719
- @overload
17536
+ @overload # type: ignore
16720
17537
  def offline_add_torrent(
16721
17538
  self,
16722
17539
  payload: str | dict,
@@ -16758,8 +17575,8 @@ class P115Client(P115OpenClient):
16758
17575
 
16759
17576
  :payload:
16760
17577
  - info_hash: str 💡 种子文件的 info_hash
16761
- - wanted: str = <default> 💡 选择文件进行下载(是数字用 "," 分隔)
16762
- - savepath: str = <default> 💡 保存到目录下的相对路径
17578
+ - wanted: str = <default> 💡 选择文件进行下载(是数字索引,从 0 开始计数,用 "," 分隔)
17579
+ - savepath: str = <default> 💡 保存到 `wp_path_id` 对应目录下的相对路径
16763
17580
  - wp_path_id: int | str = <default> 💡 保存到目录的 id
16764
17581
  """
16765
17582
  if isinstance(payload, str):
@@ -16810,7 +17627,7 @@ class P115Client(P115OpenClient):
16810
17627
  payload = {"url": payload}
16811
17628
  return self._offline_post(payload, "add_task_url", use_web_api=use_web_api, async_=async_, **request_kwargs)
16812
17629
 
16813
- @overload
17630
+ @overload # type: ignore
16814
17631
  def offline_add_urls(
16815
17632
  self,
16816
17633
  payload: str | Iterable[str] | dict,
@@ -16853,7 +17670,7 @@ class P115Client(P115OpenClient):
16853
17670
  - wp_path_id: int | str = <default> 💡 保存到目录的 id
16854
17671
  """
16855
17672
  if isinstance(payload, str):
16856
- payload = payload.strip().split("\n")
17673
+ payload = payload.strip("\n").split("\n")
16857
17674
  if not isinstance(payload, dict):
16858
17675
  payload = {f"url[{i}]": url for i, url in enumerate(payload)}
16859
17676
  if not payload:
@@ -16863,7 +17680,7 @@ class P115Client(P115OpenClient):
16863
17680
  @overload
16864
17681
  def offline_clear(
16865
17682
  self,
16866
- payload: int | dict,
17683
+ payload: int | dict = 0,
16867
17684
  /,
16868
17685
  base_url: None | bool | str | Callable[[], str] = None,
16869
17686
  *,
@@ -16874,7 +17691,7 @@ class P115Client(P115OpenClient):
16874
17691
  @overload
16875
17692
  def offline_clear(
16876
17693
  self,
16877
- payload: int | dict,
17694
+ payload: int | dict = 0,
16878
17695
  /,
16879
17696
  base_url: None | bool | str | Callable[[], str] = None,
16880
17697
  *,
@@ -16884,7 +17701,7 @@ class P115Client(P115OpenClient):
16884
17701
  ...
16885
17702
  def offline_clear(
16886
17703
  self,
16887
- payload: int | dict = {"flag": 0},
17704
+ payload: int | dict = 0,
16888
17705
  /,
16889
17706
  base_url: None | bool | str | Callable[[], str] = None,
16890
17707
  *,
@@ -16907,11 +17724,6 @@ class P115Client(P115OpenClient):
16907
17724
  """
16908
17725
  api = complete_lixian_api("?ct=lixian&ac=task_clear", base_url=base_url)
16909
17726
  if isinstance(payload, int):
16910
- flag = payload
16911
- if flag < 0:
16912
- flag = 0
16913
- elif flag > 5:
16914
- flag = 5
16915
17727
  payload = {"flag": payload}
16916
17728
  return self.request(url=api, method="POST", data=payload, async_=async_, **request_kwargs)
16917
17729
 
@@ -16985,7 +17797,7 @@ class P115Client(P115OpenClient):
16985
17797
  api = complete_api("/?ct=offline&ac=space", base_url=base_url)
16986
17798
  return self.request(url=api, async_=async_, **request_kwargs)
16987
17799
 
16988
- @overload
17800
+ @overload # type: ignore
16989
17801
  def offline_list(
16990
17802
  self,
16991
17803
  payload: int | dict = 1,
@@ -17021,7 +17833,7 @@ class P115Client(P115OpenClient):
17021
17833
  POST https://lixian.115.com/lixian/?ct=lixian&ac=task_lists
17022
17834
 
17023
17835
  :payload:
17024
- - page: int | str
17836
+ - page: int | str = 1
17025
17837
  """
17026
17838
  api = complete_lixian_api("?ct=lixian&ac=task_lists", base_url=base_url)
17027
17839
  if isinstance(payload, int):
@@ -17136,7 +17948,7 @@ class P115Client(P115OpenClient):
17136
17948
  @overload
17137
17949
  def offline_remove(
17138
17950
  self,
17139
- payload: str | dict,
17951
+ payload: str | Iterable[str] | dict,
17140
17952
  /,
17141
17953
  base_url: None | bool | str | Callable[[], str] = None,
17142
17954
  *,
@@ -17147,7 +17959,7 @@ class P115Client(P115OpenClient):
17147
17959
  @overload
17148
17960
  def offline_remove(
17149
17961
  self,
17150
- payload: str | dict,
17962
+ payload: str | Iterable[str] | dict,
17151
17963
  /,
17152
17964
  base_url: None | bool | str | Callable[[], str] = None,
17153
17965
  *,
@@ -17157,7 +17969,7 @@ class P115Client(P115OpenClient):
17157
17969
  ...
17158
17970
  def offline_remove(
17159
17971
  self,
17160
- payload: str | dict,
17972
+ payload: str | Iterable[str] | dict,
17161
17973
  /,
17162
17974
  base_url: None | bool | str | Callable[[], str] = None,
17163
17975
  *,
@@ -19522,7 +20334,7 @@ class P115Client(P115OpenClient):
19522
20334
  if resp["state"]:
19523
20335
  self.user_key = resp["data"]["userkey"]
19524
20336
  return resp
19525
- return run_gen_step(gen_step, async_=async_)
20337
+ return run_gen_step(gen_step, simple=True, async_=async_)
19526
20338
 
19527
20339
  @overload
19528
20340
  def upload_resume(
@@ -19889,7 +20701,7 @@ class P115Client(P115OpenClient):
19889
20701
  "pickcode": resp["pickcode"],
19890
20702
  }
19891
20703
  return resp
19892
- return run_gen_step(gen_step, async_=async_)
20704
+ return run_gen_step(gen_step, simple=True, async_=async_)
19893
20705
 
19894
20706
  @overload
19895
20707
  def upload_file_sample(
@@ -20025,7 +20837,7 @@ class P115Client(P115OpenClient):
20025
20837
  async_=async_,
20026
20838
  **request_kwargs,
20027
20839
  )
20028
- return run_gen_step(gen_step, async_=async_)
20840
+ return run_gen_step(gen_step, simple=True, async_=async_)
20029
20841
 
20030
20842
  @overload # type: ignore
20031
20843
  def upload_file(
@@ -20384,7 +21196,7 @@ class P115Client(P115OpenClient):
20384
21196
  async_=async_, # type: ignore
20385
21197
  **request_kwargs,
20386
21198
  )
20387
- return run_gen_step(gen_step, async_=async_)
21199
+ return run_gen_step(gen_step, simple=True, async_=async_)
20388
21200
 
20389
21201
  ########## User API ##########
20390
21202