bdpan-cli 0.1.0__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.
- bdpan_cli/__init__.py +15 -0
- bdpan_cli/_vendor/__init__.py +9 -0
- bdpan_cli/_vendor/openapi_client/__init__.py +27 -0
- bdpan_cli/_vendor/openapi_client/api/__init__.py +6 -0
- bdpan_cli/_vendor/openapi_client/api/auth_api.py +649 -0
- bdpan_cli/_vendor/openapi_client/api/fileinfo_api.py +737 -0
- bdpan_cli/_vendor/openapi_client/api/filemanager_api.py +669 -0
- bdpan_cli/_vendor/openapi_client/api/fileupload_api.py +599 -0
- bdpan_cli/_vendor/openapi_client/api/multimediafile_api.py +391 -0
- bdpan_cli/_vendor/openapi_client/api/userinfo_api.py +315 -0
- bdpan_cli/_vendor/openapi_client/api_client.py +864 -0
- bdpan_cli/_vendor/openapi_client/configuration.py +454 -0
- bdpan_cli/_vendor/openapi_client/exceptions.py +197 -0
- bdpan_cli/_vendor/openapi_client/model/__init__.py +5 -0
- bdpan_cli/_vendor/openapi_client/model/oauth_token_authorization_code_response.py +276 -0
- bdpan_cli/_vendor/openapi_client/model/oauth_token_device_code_response.py +276 -0
- bdpan_cli/_vendor/openapi_client/model/oauth_token_device_token_response.py +276 -0
- bdpan_cli/_vendor/openapi_client/model/oauth_token_refresh_token_response.py +276 -0
- bdpan_cli/_vendor/openapi_client/model/quotaresponse.py +276 -0
- bdpan_cli/_vendor/openapi_client/model/uinforesponse.py +284 -0
- bdpan_cli/_vendor/openapi_client/model_utils.py +2039 -0
- bdpan_cli/_vendor/openapi_client/rest.py +385 -0
- bdpan_cli/_version.py +3 -0
- bdpan_cli/account.py +78 -0
- bdpan_cli/apply_scrape_rename.py +99 -0
- bdpan_cli/auth.py +218 -0
- bdpan_cli/cli.py +1428 -0
- bdpan_cli/config.py +26 -0
- bdpan_cli/dedup.py +310 -0
- bdpan_cli/downloader.py +178 -0
- bdpan_cli/file_manager.py +389 -0
- bdpan_cli/foldering.py +181 -0
- bdpan_cli/logger.py +101 -0
- bdpan_cli/mcp_server.py +547 -0
- bdpan_cli/media_info.py +251 -0
- bdpan_cli/ops.py +173 -0
- bdpan_cli/organize.py +108 -0
- bdpan_cli/preview_scrape.py +182 -0
- bdpan_cli/scraper.py +505 -0
- bdpan_cli/uploader.py +391 -0
- bdpan_cli-0.1.0.dist-info/METADATA +167 -0
- bdpan_cli-0.1.0.dist-info/RECORD +44 -0
- bdpan_cli-0.1.0.dist-info/WHEEL +4 -0
- bdpan_cli-0.1.0.dist-info/entry_points.txt +5 -0
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
# !/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
xpan
|
|
4
|
+
|
|
5
|
+
xpanapi # noqa: E501
|
|
6
|
+
|
|
7
|
+
The version of the OpenAPI document: 0.1
|
|
8
|
+
Generated by: https://openapi-generator.tech
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
import io
|
|
13
|
+
import json
|
|
14
|
+
import logging
|
|
15
|
+
import re
|
|
16
|
+
import ssl
|
|
17
|
+
from urllib.parse import urlencode
|
|
18
|
+
from urllib.parse import urlparse
|
|
19
|
+
from urllib.request import proxy_bypass_environment
|
|
20
|
+
import urllib3
|
|
21
|
+
import ipaddress
|
|
22
|
+
|
|
23
|
+
from bdpan_cli._vendor.openapi_client.exceptions import ApiException, UnauthorizedException, ForbiddenException
|
|
24
|
+
from bdpan_cli._vendor.openapi_client.exceptions import NotFoundException, ServiceException, ApiValueError
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class RESTResponse(io.IOBase):
|
|
31
|
+
"""
|
|
32
|
+
class RESTResponse
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self, resp):
|
|
36
|
+
"""
|
|
37
|
+
init
|
|
38
|
+
"""
|
|
39
|
+
self.urllib3_response = resp
|
|
40
|
+
self.status = resp.status
|
|
41
|
+
self.reason = resp.reason
|
|
42
|
+
self.data = resp.data
|
|
43
|
+
|
|
44
|
+
def getheaders(self):
|
|
45
|
+
"""Returns a dictionary of the response headers."""
|
|
46
|
+
return self.urllib3_response.getheaders()
|
|
47
|
+
|
|
48
|
+
def getheader(self, name, default=None):
|
|
49
|
+
"""Returns a given response header."""
|
|
50
|
+
return self.urllib3_response.getheader(name, default)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class RESTClientObject(object):
|
|
54
|
+
"""
|
|
55
|
+
class RESTClientObject
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(self, configuration, pools_size=4, maxsize=None):
|
|
59
|
+
# urllib3.PoolManager will pass all kw parameters to connectionpool
|
|
60
|
+
# https://github.com/shazow/urllib3/blob/f9409436f83aeb79fbaf090181cd81b784f1b8ce/urllib3/poolmanager.py#L75 # noqa: E501
|
|
61
|
+
# https://github.com/shazow/urllib3/blob/f9409436f83aeb79fbaf090181cd81b784f1b8ce/urllib3/connectionpool.py#L680 # noqa: E501
|
|
62
|
+
# maxsize is the number of requests to host that are allowed in parallel # noqa: E501
|
|
63
|
+
# Custom SSL certificates and client certificates: http://urllib3.readthedocs.io/en/latest/advanced-usage.html # noqa: E501
|
|
64
|
+
|
|
65
|
+
# cert_reqs
|
|
66
|
+
if configuration.verify_ssl:
|
|
67
|
+
cert_reqs = ssl.CERT_REQUIRED
|
|
68
|
+
else:
|
|
69
|
+
cert_reqs = ssl.CERT_NONE
|
|
70
|
+
|
|
71
|
+
addition_pool_args = {}
|
|
72
|
+
if configuration.assert_hostname is not None:
|
|
73
|
+
addition_pool_args['assert_hostname'] = configuration.assert_hostname # noqa: E501
|
|
74
|
+
|
|
75
|
+
if configuration.retries is not None:
|
|
76
|
+
addition_pool_args['retries'] = configuration.retries
|
|
77
|
+
|
|
78
|
+
if configuration.socket_options is not None:
|
|
79
|
+
addition_pool_args['socket_options'] = configuration.socket_options
|
|
80
|
+
|
|
81
|
+
if maxsize is None:
|
|
82
|
+
if configuration.connection_pool_maxsize is not None:
|
|
83
|
+
maxsize = configuration.connection_pool_maxsize
|
|
84
|
+
else:
|
|
85
|
+
maxsize = 4
|
|
86
|
+
|
|
87
|
+
# https pool manager
|
|
88
|
+
if configuration.proxy and not should_bypass_proxies(configuration.host, no_proxy=configuration.no_proxy or ''):
|
|
89
|
+
self.pool_manager = urllib3.ProxyManager(
|
|
90
|
+
num_pools=pools_size,
|
|
91
|
+
maxsize=maxsize,
|
|
92
|
+
cert_reqs=cert_reqs,
|
|
93
|
+
ca_certs=configuration.ssl_ca_cert,
|
|
94
|
+
cert_file=configuration.cert_file,
|
|
95
|
+
key_file=configuration.key_file,
|
|
96
|
+
proxy_url=configuration.proxy,
|
|
97
|
+
proxy_headers=configuration.proxy_headers,
|
|
98
|
+
**addition_pool_args
|
|
99
|
+
)
|
|
100
|
+
else:
|
|
101
|
+
self.pool_manager = urllib3.PoolManager(
|
|
102
|
+
num_pools=pools_size,
|
|
103
|
+
maxsize=maxsize,
|
|
104
|
+
cert_reqs=cert_reqs,
|
|
105
|
+
ca_certs=configuration.ssl_ca_cert,
|
|
106
|
+
cert_file=configuration.cert_file,
|
|
107
|
+
key_file=configuration.key_file,
|
|
108
|
+
**addition_pool_args
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def request(self, method, url, query_params=None, headers=None,
|
|
112
|
+
body=None, post_params=None, _preload_content=True,
|
|
113
|
+
_request_timeout=None):
|
|
114
|
+
"""Perform requests.
|
|
115
|
+
|
|
116
|
+
:param method: http request method
|
|
117
|
+
:param url: http request url
|
|
118
|
+
:param query_params: query parameters in the url
|
|
119
|
+
:param headers: http request headers
|
|
120
|
+
:param body: request json body, for `application/json`
|
|
121
|
+
:param post_params: request post parameters,
|
|
122
|
+
`application/x-www-form-urlencoded`
|
|
123
|
+
and `multipart/form-data`
|
|
124
|
+
:param _preload_content: if False, the urllib3.HTTPResponse object will
|
|
125
|
+
be returned without reading/decoding response
|
|
126
|
+
data. Default is True.
|
|
127
|
+
:param _request_timeout: timeout setting for this request. If one
|
|
128
|
+
number provided, it will be total request
|
|
129
|
+
timeout. It can also be a pair (tuple) of
|
|
130
|
+
(connection, read) timeouts.
|
|
131
|
+
"""
|
|
132
|
+
method = method.upper()
|
|
133
|
+
assert method in ['GET', 'HEAD', 'DELETE', 'POST', 'PUT',
|
|
134
|
+
'PATCH', 'OPTIONS']
|
|
135
|
+
|
|
136
|
+
if post_params and body:
|
|
137
|
+
raise ApiValueError(
|
|
138
|
+
"body parameter cannot be used with post_params parameter."
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
post_params = post_params or {}
|
|
142
|
+
headers = headers or {}
|
|
143
|
+
|
|
144
|
+
timeout = None
|
|
145
|
+
if _request_timeout:
|
|
146
|
+
if isinstance(_request_timeout, (int, float)): # noqa: E501,F821
|
|
147
|
+
timeout = urllib3.Timeout(total=_request_timeout)
|
|
148
|
+
elif (isinstance(_request_timeout, tuple) and
|
|
149
|
+
len(_request_timeout) == 2):
|
|
150
|
+
timeout = urllib3.Timeout(
|
|
151
|
+
connect=_request_timeout[0], read=_request_timeout[1])
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
# For `POST`, `PUT`, `PATCH`, `OPTIONS`, `DELETE`
|
|
155
|
+
if method in ['POST', 'PUT', 'PATCH', 'OPTIONS', 'DELETE']:
|
|
156
|
+
# Only set a default Content-Type for POST, PUT, PATCH and OPTIONS requests
|
|
157
|
+
if (method != 'DELETE') and ('Content-Type' not in headers):
|
|
158
|
+
headers['Content-Type'] = 'application/json'
|
|
159
|
+
if query_params:
|
|
160
|
+
url += '&' + urlencode(query_params)
|
|
161
|
+
# url += '?' + urlencode(query_params)
|
|
162
|
+
if ('Content-Type' not in headers) or (re.search('json', headers['Content-Type'], re.IGNORECASE)):
|
|
163
|
+
request_body = None
|
|
164
|
+
if body is not None:
|
|
165
|
+
request_body = json.dumps(body)
|
|
166
|
+
r = self.pool_manager.request(
|
|
167
|
+
method, url,
|
|
168
|
+
body=request_body,
|
|
169
|
+
preload_content=_preload_content,
|
|
170
|
+
timeout=timeout,
|
|
171
|
+
headers=headers)
|
|
172
|
+
elif headers['Content-Type'] == 'application/x-www-form-urlencoded': # noqa: E501
|
|
173
|
+
r = self.pool_manager.request(
|
|
174
|
+
method, url,
|
|
175
|
+
fields=post_params,
|
|
176
|
+
encode_multipart=False,
|
|
177
|
+
preload_content=_preload_content,
|
|
178
|
+
timeout=timeout,
|
|
179
|
+
headers=headers)
|
|
180
|
+
elif headers['Content-Type'] == 'multipart/form-data':
|
|
181
|
+
# must del headers['Content-Type'], or the correct
|
|
182
|
+
# Content-Type which generated by urllib3 will be
|
|
183
|
+
# overwritten.
|
|
184
|
+
del headers['Content-Type']
|
|
185
|
+
r = self.pool_manager.request(
|
|
186
|
+
method, url,
|
|
187
|
+
fields=post_params,
|
|
188
|
+
encode_multipart=True,
|
|
189
|
+
preload_content=_preload_content,
|
|
190
|
+
timeout=timeout,
|
|
191
|
+
headers=headers)
|
|
192
|
+
# Pass a `string` parameter directly in the body to support
|
|
193
|
+
# other content types than Json when `body` argument is
|
|
194
|
+
# provided in serialized form
|
|
195
|
+
elif isinstance(body, str) or isinstance(body, bytes):
|
|
196
|
+
request_body = body
|
|
197
|
+
r = self.pool_manager.request(
|
|
198
|
+
method, url,
|
|
199
|
+
body=request_body,
|
|
200
|
+
preload_content=_preload_content,
|
|
201
|
+
timeout=timeout,
|
|
202
|
+
headers=headers)
|
|
203
|
+
else:
|
|
204
|
+
# Cannot generate the request from given parameters
|
|
205
|
+
msg = """Cannot prepare a request message for provided
|
|
206
|
+
arguments. Please check that your arguments match
|
|
207
|
+
declared content type."""
|
|
208
|
+
raise ApiException(status=0, reason=msg)
|
|
209
|
+
# For `GET`, `HEAD`
|
|
210
|
+
else:
|
|
211
|
+
r = self.pool_manager.request(method, url,
|
|
212
|
+
# fields=query_params,
|
|
213
|
+
preload_content=_preload_content,
|
|
214
|
+
timeout=timeout,
|
|
215
|
+
headers=headers)
|
|
216
|
+
except urllib3.exceptions.SSLError as e:
|
|
217
|
+
msg = "{0}\n{1}".format(type(e).__name__, str(e))
|
|
218
|
+
raise ApiException(status=0, reason=msg)
|
|
219
|
+
|
|
220
|
+
if _preload_content:
|
|
221
|
+
r = RESTResponse(r)
|
|
222
|
+
|
|
223
|
+
# log response body
|
|
224
|
+
logger.debug("response body: %s", r.data)
|
|
225
|
+
|
|
226
|
+
if not 200 <= r.status <= 299:
|
|
227
|
+
if r.status == 401:
|
|
228
|
+
raise UnauthorizedException(http_resp=r)
|
|
229
|
+
|
|
230
|
+
if r.status == 403:
|
|
231
|
+
raise ForbiddenException(http_resp=r)
|
|
232
|
+
|
|
233
|
+
if r.status == 404:
|
|
234
|
+
raise NotFoundException(http_resp=r)
|
|
235
|
+
|
|
236
|
+
if 500 <= r.status <= 599:
|
|
237
|
+
raise ServiceException(http_resp=r)
|
|
238
|
+
|
|
239
|
+
raise ApiException(http_resp=r)
|
|
240
|
+
|
|
241
|
+
return r
|
|
242
|
+
|
|
243
|
+
def GET(self, url, headers=None, query_params=None, _preload_content=True,
|
|
244
|
+
_request_timeout=None):
|
|
245
|
+
"""
|
|
246
|
+
HTTP Get
|
|
247
|
+
"""
|
|
248
|
+
if query_params:
|
|
249
|
+
url += '&' + urlencode(query_params)
|
|
250
|
+
# print(url)
|
|
251
|
+
return self.request("GET", url,
|
|
252
|
+
headers=headers,
|
|
253
|
+
_preload_content=_preload_content,
|
|
254
|
+
_request_timeout=_request_timeout,
|
|
255
|
+
query_params=query_params)
|
|
256
|
+
|
|
257
|
+
def HEAD(self, url, headers=None, query_params=None, _preload_content=True,
|
|
258
|
+
_request_timeout=None):
|
|
259
|
+
"""
|
|
260
|
+
HTTP HEAD
|
|
261
|
+
"""
|
|
262
|
+
return self.request("HEAD", url,
|
|
263
|
+
headers=headers,
|
|
264
|
+
_preload_content=_preload_content,
|
|
265
|
+
_request_timeout=_request_timeout,
|
|
266
|
+
query_params=query_params)
|
|
267
|
+
|
|
268
|
+
def OPTIONS(self, url, headers=None, query_params=None, post_params=None,
|
|
269
|
+
body=None, _preload_content=True, _request_timeout=None):
|
|
270
|
+
"""
|
|
271
|
+
HTTP OPTIONS
|
|
272
|
+
"""
|
|
273
|
+
return self.request("OPTIONS", url,
|
|
274
|
+
headers=headers,
|
|
275
|
+
query_params=query_params,
|
|
276
|
+
post_params=post_params,
|
|
277
|
+
_preload_content=_preload_content,
|
|
278
|
+
_request_timeout=_request_timeout,
|
|
279
|
+
body=body)
|
|
280
|
+
|
|
281
|
+
def DELETE(self, url, headers=None, query_params=None, body=None,
|
|
282
|
+
_preload_content=True, _request_timeout=None):
|
|
283
|
+
"""
|
|
284
|
+
HTTP DELETE
|
|
285
|
+
"""
|
|
286
|
+
return self.request("DELETE", url,
|
|
287
|
+
headers=headers,
|
|
288
|
+
query_params=query_params,
|
|
289
|
+
_preload_content=_preload_content,
|
|
290
|
+
_request_timeout=_request_timeout,
|
|
291
|
+
body=body)
|
|
292
|
+
|
|
293
|
+
def POST(self, url, headers=None, query_params=None, post_params=None,
|
|
294
|
+
body=None, _preload_content=True, _request_timeout=None):
|
|
295
|
+
"""
|
|
296
|
+
HTTP POST
|
|
297
|
+
"""
|
|
298
|
+
return self.request("POST", url,
|
|
299
|
+
headers = headers,
|
|
300
|
+
query_params = query_params,
|
|
301
|
+
post_params = post_params,
|
|
302
|
+
_preload_content = _preload_content,
|
|
303
|
+
_request_timeout = _request_timeout,
|
|
304
|
+
body = body)
|
|
305
|
+
|
|
306
|
+
def PUT(self, url, headers = None, query_params = None, post_params = None,
|
|
307
|
+
body=None, _preload_content=True, _request_timeout=None):
|
|
308
|
+
"""
|
|
309
|
+
HTTP PUT
|
|
310
|
+
"""
|
|
311
|
+
return self.request("PUT", url,
|
|
312
|
+
headers=headers,
|
|
313
|
+
query_params=query_params,
|
|
314
|
+
post_params=post_params,
|
|
315
|
+
_preload_content=_preload_content,
|
|
316
|
+
_request_timeout=_request_timeout,
|
|
317
|
+
body=body)
|
|
318
|
+
|
|
319
|
+
def PATCH(self, url, headers=None, query_params=None, post_params=None,
|
|
320
|
+
body=None, _preload_content=True, _request_timeout=None):
|
|
321
|
+
"""
|
|
322
|
+
HTTP PATCH
|
|
323
|
+
"""
|
|
324
|
+
return self.request("PATCH", url,
|
|
325
|
+
headers=headers,
|
|
326
|
+
query_params=query_params,
|
|
327
|
+
post_params=post_params,
|
|
328
|
+
_preload_content=_preload_content,
|
|
329
|
+
_request_timeout=_request_timeout,
|
|
330
|
+
body=body)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
# end of class RESTClientObject
|
|
334
|
+
def is_ipv4(target):
|
|
335
|
+
""" Test if IPv4 address or not
|
|
336
|
+
"""
|
|
337
|
+
try:
|
|
338
|
+
chk = ipaddress.IPv4Address(target)
|
|
339
|
+
return True
|
|
340
|
+
except ipaddress.AddressValueError:
|
|
341
|
+
return False
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def in_ipv4net(target, net):
|
|
345
|
+
""" Test if target belongs to given IPv4 network
|
|
346
|
+
"""
|
|
347
|
+
try:
|
|
348
|
+
nw = ipaddress.IPv4Network(net)
|
|
349
|
+
ip = ipaddress.IPv4Address(target)
|
|
350
|
+
if ip in nw:
|
|
351
|
+
return True
|
|
352
|
+
return False
|
|
353
|
+
except ipaddress.AddressValueError:
|
|
354
|
+
return False
|
|
355
|
+
except ipaddress.NetmaskValueError:
|
|
356
|
+
return False
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def should_bypass_proxies(url, no_proxy=None):
|
|
360
|
+
""" Yet another requests.should_bypass_proxies
|
|
361
|
+
Test if proxies should not be used for a particular url.
|
|
362
|
+
"""
|
|
363
|
+
|
|
364
|
+
parsed = urlparse(url)
|
|
365
|
+
|
|
366
|
+
# special cases
|
|
367
|
+
if parsed.hostname in [None, '']:
|
|
368
|
+
return True
|
|
369
|
+
|
|
370
|
+
# special cases
|
|
371
|
+
if no_proxy in [None ,'']:
|
|
372
|
+
return False
|
|
373
|
+
if no_proxy == '*':
|
|
374
|
+
return True
|
|
375
|
+
|
|
376
|
+
no_proxy = no_proxy.lower().replace(' ', '');
|
|
377
|
+
entries = (
|
|
378
|
+
host for host in no_proxy.split(',') if host
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
if is_ipv4(parsed.hostname):
|
|
382
|
+
for item in entries:
|
|
383
|
+
if in_ipv4net(parsed.hostname, item):
|
|
384
|
+
return True
|
|
385
|
+
return proxy_bypass_environment(parsed.hostname, {'no': no_proxy})
|
bdpan_cli/_version.py
ADDED
bdpan_cli/account.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""账户信息客户端,封装网盘容量与用户信息查询。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from bdpan_cli._vendor import openapi_client
|
|
8
|
+
from bdpan_cli._vendor.openapi_client.api.userinfo_api import UserinfoApi
|
|
9
|
+
from bdpan_cli._vendor.openapi_client.rest import ApiException
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AccountError(RuntimeError):
|
|
13
|
+
"""账户信息查询异常。"""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AccountClient:
|
|
17
|
+
"""网盘账户信息封装(容量配额、用户资料)。"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, access_token: str, api_client: openapi_client.ApiClient | None = None) -> None:
|
|
20
|
+
self.access_token = access_token
|
|
21
|
+
self.api_client = api_client or openapi_client.ApiClient()
|
|
22
|
+
self._userinfo_api = UserinfoApi(self.api_client)
|
|
23
|
+
|
|
24
|
+
def quota(self, check_expire: bool = False, check_free: bool = False) -> dict[str, Any]:
|
|
25
|
+
"""查询网盘容量配额。
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
check_expire: 同时返回会员容量过期时间(expire 字段)。
|
|
29
|
+
check_free: 同时返回免费容量配额(free 字段)。
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
含 total / used(字节)等键的容量信息字典。
|
|
33
|
+
"""
|
|
34
|
+
try:
|
|
35
|
+
res = self._userinfo_api.apiquota(
|
|
36
|
+
access_token=self.access_token,
|
|
37
|
+
checkexpire=1 if check_expire else 0,
|
|
38
|
+
checkfree=1 if check_free else 0,
|
|
39
|
+
)
|
|
40
|
+
except ApiException as e:
|
|
41
|
+
raise AccountError(f"查询网盘容量失败: {e.status} {e.body}") from e
|
|
42
|
+
return self._validate(self._normalize(res), "查询网盘容量")
|
|
43
|
+
|
|
44
|
+
def user_info(self) -> dict[str, Any]:
|
|
45
|
+
"""查询网盘用户信息(百度账号、网盘名、会员类型)。
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
含 baidu_name / netdisk_name / vip_type 等键的用户信息字典。
|
|
49
|
+
"""
|
|
50
|
+
try:
|
|
51
|
+
res = self._userinfo_api.xpannasuinfo(access_token=self.access_token)
|
|
52
|
+
except ApiException as e:
|
|
53
|
+
raise AccountError(f"查询用户信息失败: {e.status} {e.body}") from e
|
|
54
|
+
return self._validate(self._normalize(res), "查询用户信息")
|
|
55
|
+
|
|
56
|
+
@staticmethod
|
|
57
|
+
def _normalize(res: Any) -> dict[str, Any]:
|
|
58
|
+
"""统一响应形态。
|
|
59
|
+
|
|
60
|
+
UserinfoApi 经 SDK 反序列化返回模型对象而非裸 dict(区别于
|
|
61
|
+
FileinfoApi 等直接返回 dict 的接口),需归一化:优先 to_dict(),
|
|
62
|
+
退化按 attribute_map 逐键取值。
|
|
63
|
+
"""
|
|
64
|
+
if isinstance(res, dict):
|
|
65
|
+
return res
|
|
66
|
+
to_dict = getattr(res, "to_dict", None)
|
|
67
|
+
if callable(to_dict):
|
|
68
|
+
return to_dict()
|
|
69
|
+
attr_map = getattr(res, "attribute_map", None)
|
|
70
|
+
if isinstance(attr_map, dict):
|
|
71
|
+
return {key: getattr(res, key, None) for key in attr_map}
|
|
72
|
+
raise AccountError(f"无法解析的响应类型: {type(res).__name__}")
|
|
73
|
+
|
|
74
|
+
@staticmethod
|
|
75
|
+
def _validate(res: dict[str, Any], ctx: str) -> dict[str, Any]:
|
|
76
|
+
if res.get("errno") not in (0, None):
|
|
77
|
+
raise AccountError(f"{ctx}返回错误: errno={res.get('errno')} ({res})")
|
|
78
|
+
return res
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""依据审核后的 scrape_preview.json 执行网盘目录批量重命名。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
from bdpan_cli.auth import TokenManager
|
|
8
|
+
from bdpan_cli.config import load_config
|
|
9
|
+
from bdpan_cli.file_manager import FileManager
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
ROOT_UNWATCHED = "/影视/电影/unwatched"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def apply_renames(
|
|
17
|
+
preview_file: str = "scrape_preview.json",
|
|
18
|
+
target_dir: str | None = None,
|
|
19
|
+
batch_size: int = 50,
|
|
20
|
+
) -> None:
|
|
21
|
+
config = load_config()
|
|
22
|
+
token = TokenManager(config).get_access_token()
|
|
23
|
+
mgr = FileManager(token)
|
|
24
|
+
|
|
25
|
+
with open(preview_file, "r", encoding="utf-8") as f:
|
|
26
|
+
mapping = json.load(f)
|
|
27
|
+
|
|
28
|
+
verify_dir = target_dir or (mapping[0].get("parent_dir") if mapping else None) or ROOT_UNWATCHED
|
|
29
|
+
logger.info("正在查询网盘当前 %s 目录下实际存在的条目...", verify_dir)
|
|
30
|
+
current_entries = mgr.list_dir(verify_dir)
|
|
31
|
+
current_names = set(e.get("server_filename") for e in current_entries)
|
|
32
|
+
logger.info("网盘当前 %s 目录下实际存在 %d 个条目", verify_dir, len(current_names))
|
|
33
|
+
|
|
34
|
+
rename_items: list[tuple[str, str]] = []
|
|
35
|
+
skipped_items: list[str] = []
|
|
36
|
+
|
|
37
|
+
for item in mapping:
|
|
38
|
+
idx = item["index"]
|
|
39
|
+
raw = item["raw_name"]
|
|
40
|
+
target = item["target_name"]
|
|
41
|
+
status = item.get("status")
|
|
42
|
+
|
|
43
|
+
if status != "OK" or not target or target.startswith("【"):
|
|
44
|
+
logger.warning("[%d] 跳过未匹配或跳过项 (%s): %s", idx, status, raw)
|
|
45
|
+
skipped_items.append(raw)
|
|
46
|
+
continue
|
|
47
|
+
|
|
48
|
+
if raw == target or target in current_names:
|
|
49
|
+
logger.info("[%d] 目标名称已符合规范或已存在,无需更名: %s", idx, target)
|
|
50
|
+
skipped_items.append(raw)
|
|
51
|
+
continue
|
|
52
|
+
|
|
53
|
+
if raw not in current_names:
|
|
54
|
+
logger.warning("[%d] 源目录在网盘中不存在,跳过: %s", idx, raw)
|
|
55
|
+
skipped_items.append(raw)
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
src_path = item.get("src_path") or f"{verify_dir}/{raw}"
|
|
59
|
+
rename_items.append((src_path, target))
|
|
60
|
+
logger.info("[%d] 计划更名: %s -> %s", idx, raw, target)
|
|
61
|
+
|
|
62
|
+
if not rename_items:
|
|
63
|
+
logger.info("没有需要更名的目录,操作完成。")
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
logger.info("准备对 %d 个目录执行批量更名(每批 %d 项)...", len(rename_items), batch_size)
|
|
67
|
+
results = mgr.batch_rename(rename_items, batch_size=batch_size)
|
|
68
|
+
|
|
69
|
+
# 统计成功与失败
|
|
70
|
+
success_count = 0
|
|
71
|
+
fail_count = 0
|
|
72
|
+
for r in results:
|
|
73
|
+
info_list = r.get("info", [])
|
|
74
|
+
for inf in info_list:
|
|
75
|
+
if inf.get("errno") == 0:
|
|
76
|
+
success_count += 1
|
|
77
|
+
else:
|
|
78
|
+
fail_count += 1
|
|
79
|
+
logger.error("更名失败项: %s", inf)
|
|
80
|
+
|
|
81
|
+
logger.info("=== 批量更名执行完毕: 提交 %d, 成功 %d, 失败 %d, 跳过 %d ===", len(rename_items), success_count, fail_count, len(skipped_items))
|
|
82
|
+
|
|
83
|
+
# 执行对账
|
|
84
|
+
logger.info("=== 执行网盘目录更名后对账 ===")
|
|
85
|
+
reconciled_entries = mgr.list_dir(verify_dir)
|
|
86
|
+
logger.info("当前 %s 下共有 %d 个目录:", verify_dir, len(reconciled_entries))
|
|
87
|
+
for i, e in enumerate(reconciled_entries, 1):
|
|
88
|
+
logger.info(" %2d. %s", i, e.get("server_filename"))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
if __name__ == "__main__":
|
|
92
|
+
import argparse
|
|
93
|
+
parser = argparse.ArgumentParser(description="执行批量更名")
|
|
94
|
+
parser.add_argument("--preview", default="watched_scrape_preview.json", help="预演映射文件")
|
|
95
|
+
parser.add_argument("--dir", default=None, help="目标目录(可选,若未在 json 中指定)")
|
|
96
|
+
parser.add_argument("--batch-size", type=int, default=25, help="每批重命名数量")
|
|
97
|
+
args = parser.parse_args()
|
|
98
|
+
apply_renames(args.preview, target_dir=args.dir, batch_size=args.batch_size)
|
|
99
|
+
|