]> jfr.im git - yt-dlp.git/blame - youtube_dl/compat.py
[iqiyi] Improve error detection for VIP-only videos
[yt-dlp.git] / youtube_dl / compat.py
CommitLineData
451948b2
PH
1from __future__ import unicode_literals
2
0a67a363 3import binascii
003c69a8 4import collections
0a67a363 5import email
8c25f81b 6import getpass
0a67a363 7import io
e07e9313 8import optparse
8c25f81b 9import os
7d4111ed 10import re
51f579b6 11import shlex
003c69a8 12import shutil
be4a824d 13import socket
8c25f81b
PH
14import subprocess
15import sys
a0e060ac 16import itertools
36e6f62c 17import xml.etree.ElementTree
8c25f81b
PH
18
19
20try:
21 import urllib.request as compat_urllib_request
5f6a1245 22except ImportError: # Python 2
8c25f81b
PH
23 import urllib2 as compat_urllib_request
24
25try:
26 import urllib.error as compat_urllib_error
5f6a1245 27except ImportError: # Python 2
8c25f81b
PH
28 import urllib2 as compat_urllib_error
29
30try:
31 import urllib.parse as compat_urllib_parse
5f6a1245 32except ImportError: # Python 2
8c25f81b
PH
33 import urllib as compat_urllib_parse
34
35try:
36 from urllib.parse import urlparse as compat_urllib_parse_urlparse
5f6a1245 37except ImportError: # Python 2
8c25f81b
PH
38 from urlparse import urlparse as compat_urllib_parse_urlparse
39
40try:
41 import urllib.parse as compat_urlparse
5f6a1245 42except ImportError: # Python 2
8c25f81b
PH
43 import urlparse as compat_urlparse
44
0a67a363
YCH
45try:
46 import urllib.response as compat_urllib_response
47except ImportError: # Python 2
48 import urllib as compat_urllib_response
49
8c25f81b
PH
50try:
51 import http.cookiejar as compat_cookiejar
5f6a1245 52except ImportError: # Python 2
8c25f81b
PH
53 import cookielib as compat_cookiejar
54
799207e8 55try:
56 import http.cookies as compat_cookies
57except ImportError: # Python 2
58 import Cookie as compat_cookies
59
8c25f81b
PH
60try:
61 import html.entities as compat_html_entities
5f6a1245 62except ImportError: # Python 2
8c25f81b
PH
63 import htmlentitydefs as compat_html_entities
64
8c25f81b
PH
65try:
66 import http.client as compat_http_client
5f6a1245 67except ImportError: # Python 2
8c25f81b
PH
68 import httplib as compat_http_client
69
70try:
71 from urllib.error import HTTPError as compat_HTTPError
72except ImportError: # Python 2
73 from urllib2 import HTTPError as compat_HTTPError
74
75try:
76 from urllib.request import urlretrieve as compat_urlretrieve
77except ImportError: # Python 2
78 from urllib import urlretrieve as compat_urlretrieve
79
8bb56eee
BF
80try:
81 from html.parser import HTMLParser as compat_HTMLParser
82except ImportError: # Python 2
83 from HTMLParser import HTMLParser as compat_HTMLParser
84
8c25f81b
PH
85
86try:
87 from subprocess import DEVNULL
88 compat_subprocess_get_DEVNULL = lambda: DEVNULL
89except ImportError:
90 compat_subprocess_get_DEVNULL = lambda: open(os.path.devnull, 'w')
91
83fda3c0
PH
92try:
93 import http.server as compat_http_server
94except ImportError:
95 import BaseHTTPServer as compat_http_server
96
953fed28
PH
97try:
98 compat_str = unicode # Python 2
99except NameError:
100 compat_str = str
101
8c25f81b 102try:
55139679 103 from urllib.parse import unquote_to_bytes as compat_urllib_parse_unquote_to_bytes
8c25f81b 104 from urllib.parse import unquote as compat_urllib_parse_unquote
aa99aa4e 105 from urllib.parse import unquote_plus as compat_urllib_parse_unquote_plus
55139679 106except ImportError: # Python 2
22603348
S
107 _asciire = (compat_urllib_parse._asciire if hasattr(compat_urllib_parse, '_asciire')
108 else re.compile('([\x00-\x7f]+)'))
3cc8b4c3 109
4d08161a 110 # HACK: The following are the correct unquote_to_bytes, unquote and unquote_plus
55139679
S
111 # implementations from cpython 3.4.3's stdlib. Python 2's version
112 # is apparently broken (see https://github.com/rg3/youtube-dl/pull/6244)
113
c9c854ce 114 def compat_urllib_parse_unquote_to_bytes(string):
115 """unquote_to_bytes('abc%20def') -> b'abc def'."""
116 # Note: strings are encoded as UTF-8. This is only an issue if it contains
117 # unescaped non-ASCII characters, which URIs should not.
118 if not string:
119 # Is it a string-like object?
120 string.split
121 return b''
953fed28 122 if isinstance(string, compat_str):
c9c854ce 123 string = string.encode('utf-8')
55139679 124 bits = string.split(b'%')
c9c854ce 125 if len(bits) == 1:
126 return string
127 res = [bits[0]]
128 append = res.append
c9c854ce 129 for item in bits[1:]:
130 try:
55139679 131 append(compat_urllib_parse._hextochr[item[:2]])
c9c854ce 132 append(item[2:])
55139679 133 except KeyError:
c9c854ce 134 append(b'%')
135 append(item)
136 return b''.join(res)
137
a0f28f90 138 def compat_urllib_parse_unquote(string, encoding='utf-8', errors='replace'):
c9c854ce 139 """Replace %xx escapes by their single-character equivalent. The optional
140 encoding and errors parameters specify how to decode percent-encoded
141 sequences into Unicode characters, as accepted by the bytes.decode()
142 method.
143 By default, percent-encoded sequences are decoded with UTF-8, and invalid
144 sequences are replaced by a placeholder character.
145
146 unquote('abc%20def') -> 'abc def'.
147 """
c9c854ce 148 if '%' not in string:
149 string.split
150 return string
151 if encoding is None:
152 encoding = 'utf-8'
153 if errors is None:
154 errors = 'replace'
3cc8b4c3 155 bits = _asciire.split(string)
c9c854ce 156 res = [bits[0]]
157 append = res.append
158 for i in range(1, len(bits), 2):
55139679
S
159 append(compat_urllib_parse_unquote_to_bytes(bits[i]).decode(encoding, errors))
160 append(bits[i + 1])
c9c854ce 161 return ''.join(res)
162
aa99aa4e
S
163 def compat_urllib_parse_unquote_plus(string, encoding='utf-8', errors='replace'):
164 """Like unquote(), but also replace plus signs by spaces, as required for
165 unquoting HTML form values.
166
167 unquote_plus('%7e/abc+def') -> '~/abc def'
168 """
169 string = string.replace('+', ' ')
170 return compat_urllib_parse_unquote(string, encoding, errors)
171
15707c7e
S
172try:
173 from urllib.parse import urlencode as compat_urllib_parse_urlencode
174except ImportError: # Python 2
175 # Python 2 will choke in urlencode on mixture of byte and unicode strings.
176 # Possible solutions are to either port it from python 3 with all
177 # the friends or manually ensure input query contains only byte strings.
178 # We will stick with latter thus recursively encoding the whole query.
179 def compat_urllib_parse_urlencode(query, doseq=0, encoding='utf-8'):
180 def encode_elem(e):
181 if isinstance(e, dict):
182 e = encode_dict(e)
183 elif isinstance(e, (list, tuple,)):
184 e = encode_list(e)
185 elif isinstance(e, compat_str):
186 e = e.encode(encoding)
187 return e
188
189 def encode_dict(d):
190 return dict((encode_elem(k), encode_elem(v)) for k, v in d.items())
191
192 def encode_list(l):
193 return [encode_elem(e) for e in l]
194
195 return compat_urllib_parse.urlencode(encode_elem(query), doseq=doseq)
196
0a67a363
YCH
197try:
198 from urllib.request import DataHandler as compat_urllib_request_DataHandler
199except ImportError: # Python < 3.4
200 # Ported from CPython 98774:1733b3bd46db, Lib/urllib/request.py
201 class compat_urllib_request_DataHandler(compat_urllib_request.BaseHandler):
202 def data_open(self, req):
203 # data URLs as specified in RFC 2397.
204 #
205 # ignores POSTed data
206 #
207 # syntax:
208 # dataurl := "data:" [ mediatype ] [ ";base64" ] "," data
209 # mediatype := [ type "/" subtype ] *( ";" parameter )
210 # data := *urlchar
211 # parameter := attribute "=" value
212 url = req.get_full_url()
213
611c1dd9
S
214 scheme, data = url.split(':', 1)
215 mediatype, data = data.split(',', 1)
0a67a363
YCH
216
217 # even base64 encoded data URLs might be quoted so unquote in any case:
218 data = compat_urllib_parse_unquote_to_bytes(data)
611c1dd9 219 if mediatype.endswith(';base64'):
0a67a363
YCH
220 data = binascii.a2b_base64(data)
221 mediatype = mediatype[:-7]
222
223 if not mediatype:
611c1dd9 224 mediatype = 'text/plain;charset=US-ASCII'
0a67a363
YCH
225
226 headers = email.message_from_string(
611c1dd9 227 'Content-type: %s\nContent-length: %d\n' % (mediatype, len(data)))
0a67a363
YCH
228
229 return compat_urllib_response.addinfourl(io.BytesIO(data), headers, url)
230
8f9312c3 231try:
0196149c 232 compat_basestring = basestring # Python 2
8f9312c3 233except NameError:
0196149c 234 compat_basestring = str
8f9312c3
PH
235
236try:
237 compat_chr = unichr # Python 2
238except NameError:
239 compat_chr = chr
240
241try:
242 from xml.etree.ElementTree import ParseError as compat_xml_parse_error
243except ImportError: # Python 2.6
244 from xml.parsers.expat import ExpatError as compat_xml_parse_error
245
36e6f62c
JMF
246if sys.version_info[0] >= 3:
247 compat_etree_fromstring = xml.etree.ElementTree.fromstring
248else:
ae37338e
JMF
249 # python 2.x tries to encode unicode strings with ascii (see the
250 # XMLParser._fixtext method)
36e6f62c
JMF
251 etree = xml.etree.ElementTree
252
f7854627
JMF
253 try:
254 _etree_iter = etree.Element.iter
255 except AttributeError: # Python <=2.6
256 def _etree_iter(root):
257 for el in root.findall('*'):
258 yield el
259 for sub in _etree_iter(el):
260 yield sub
261
36e6f62c
JMF
262 # on 2.6 XML doesn't have a parser argument, function copied from CPython
263 # 2.7 source
264 def _XML(text, parser=None):
265 if not parser:
266 parser = etree.XMLParser(target=etree.TreeBuilder())
267 parser.feed(text)
268 return parser.close()
269
270 def _element_factory(*args, **kwargs):
271 el = etree.Element(*args, **kwargs)
272 for k, v in el.items():
387db16a
JMF
273 if isinstance(v, bytes):
274 el.set(k, v.decode('utf-8'))
36e6f62c
JMF
275 return el
276
277 def compat_etree_fromstring(text):
f7854627
JMF
278 doc = _XML(text, parser=etree.XMLParser(target=etree.TreeBuilder(element_factory=_element_factory)))
279 for el in _etree_iter(doc):
280 if el.text is not None and isinstance(el.text, bytes):
281 el.text = el.text.decode('utf-8')
282 return doc
8c25f81b 283
57f7e3c6
S
284if sys.version_info < (2, 7):
285 # Here comes the crazy part: In 2.6, if the xpath is a unicode,
286 # .//node does not match if a node is a direct child of . !
287 def compat_xpath(xpath):
288 if isinstance(xpath, compat_str):
289 xpath = xpath.encode('ascii')
290 return xpath
291else:
292 compat_xpath = lambda xpath: xpath
293
8c25f81b
PH
294try:
295 from urllib.parse import parse_qs as compat_parse_qs
5f6a1245 296except ImportError: # Python 2
8c25f81b
PH
297 # HACK: The following is the correct parse_qs implementation from cpython 3's stdlib.
298 # Python 2's version is apparently totally broken
299
300 def _parse_qsl(qs, keep_blank_values=False, strict_parsing=False,
9e1a5b84 301 encoding='utf-8', errors='replace'):
8f9312c3 302 qs, _coerce_result = qs, compat_str
8c25f81b
PH
303 pairs = [s2 for s1 in qs.split('&') for s2 in s1.split(';')]
304 r = []
305 for name_value in pairs:
306 if not name_value and not strict_parsing:
307 continue
308 nv = name_value.split('=', 1)
309 if len(nv) != 2:
310 if strict_parsing:
611c1dd9 311 raise ValueError('bad query field: %r' % (name_value,))
8c25f81b
PH
312 # Handle case of a control-name with no equal sign
313 if keep_blank_values:
314 nv.append('')
315 else:
316 continue
317 if len(nv[1]) or keep_blank_values:
318 name = nv[0].replace('+', ' ')
319 name = compat_urllib_parse_unquote(
320 name, encoding=encoding, errors=errors)
321 name = _coerce_result(name)
322 value = nv[1].replace('+', ' ')
323 value = compat_urllib_parse_unquote(
324 value, encoding=encoding, errors=errors)
325 value = _coerce_result(value)
326 r.append((name, value))
327 return r
328
329 def compat_parse_qs(qs, keep_blank_values=False, strict_parsing=False,
9e1a5b84 330 encoding='utf-8', errors='replace'):
8c25f81b
PH
331 parsed_result = {}
332 pairs = _parse_qsl(qs, keep_blank_values, strict_parsing,
9e1a5b84 333 encoding=encoding, errors=errors)
8c25f81b
PH
334 for name, value in pairs:
335 if name in parsed_result:
336 parsed_result[name].append(value)
337 else:
338 parsed_result[name] = [value]
339 return parsed_result
340
8c25f81b
PH
341try:
342 from shlex import quote as shlex_quote
343except ImportError: # Python < 3.3
344 def shlex_quote(s):
7d4111ed
PH
345 if re.match(r'^[-_\w./]+$', s):
346 return s
347 else:
348 return "'" + s.replace("'", "'\"'\"'") + "'"
8c25f81b
PH
349
350
8df5ae15 351if sys.version_info >= (2, 7, 3):
51f579b6
S
352 compat_shlex_split = shlex.split
353else:
354 # Working around shlex issue with unicode strings on some python 2
355 # versions (see http://bugs.python.org/issue1548891)
356 def compat_shlex_split(s, comments=False, posix=True):
953fed28 357 if isinstance(s, compat_str):
51f579b6
S
358 s = s.encode('utf-8')
359 return shlex.split(s, comments, posix)
360
361
8c25f81b 362def compat_ord(c):
5f6a1245
JW
363 if type(c) is int:
364 return c
365 else:
366 return ord(c)
8c25f81b
PH
367
368
e9c0cdd3
YCH
369compat_os_name = os._name if os.name == 'java' else os.name
370
371
8c25f81b
PH
372if sys.version_info >= (3, 0):
373 compat_getenv = os.getenv
374 compat_expanduser = os.path.expanduser
375else:
376 # Environment variables should be decoded with filesystem encoding.
377 # Otherwise it will fail if any non-ASCII characters present (see #3854 #3217 #2918)
378
379 def compat_getenv(key, default=None):
380 from .utils import get_filesystem_encoding
381 env = os.getenv(key, default)
382 if env:
383 env = env.decode(get_filesystem_encoding())
384 return env
385
386 # HACK: The default implementations of os.path.expanduser from cpython do not decode
387 # environment variables with filesystem encoding. We will work around this by
388 # providing adjusted implementations.
389 # The following are os.path.expanduser implementations from cpython 2.7.8 stdlib
390 # for different platforms with correct environment variables decoding.
391
e9c0cdd3 392 if compat_os_name == 'posix':
8c25f81b
PH
393 def compat_expanduser(path):
394 """Expand ~ and ~user constructions. If user or $HOME is unknown,
395 do nothing."""
396 if not path.startswith('~'):
397 return path
398 i = path.find('/', 1)
399 if i < 0:
400 i = len(path)
401 if i == 1:
402 if 'HOME' not in os.environ:
403 import pwd
404 userhome = pwd.getpwuid(os.getuid()).pw_dir
405 else:
406 userhome = compat_getenv('HOME')
407 else:
408 import pwd
409 try:
410 pwent = pwd.getpwnam(path[1:i])
411 except KeyError:
412 return path
413 userhome = pwent.pw_dir
414 userhome = userhome.rstrip('/')
415 return (userhome + path[i:]) or '/'
e9c0cdd3 416 elif compat_os_name == 'nt' or compat_os_name == 'ce':
8c25f81b
PH
417 def compat_expanduser(path):
418 """Expand ~ and ~user constructs.
419
420 If user or $HOME is unknown, do nothing."""
421 if path[:1] != '~':
422 return path
423 i, n = 1, len(path)
424 while i < n and path[i] not in '/\\':
425 i = i + 1
426
427 if 'HOME' in os.environ:
428 userhome = compat_getenv('HOME')
429 elif 'USERPROFILE' in os.environ:
430 userhome = compat_getenv('USERPROFILE')
83e865a3 431 elif 'HOMEPATH' not in os.environ:
8c25f81b
PH
432 return path
433 else:
434 try:
435 drive = compat_getenv('HOMEDRIVE')
436 except KeyError:
437 drive = ''
438 userhome = os.path.join(drive, compat_getenv('HOMEPATH'))
439
5f6a1245 440 if i != 1: # ~user
8c25f81b
PH
441 userhome = os.path.join(os.path.dirname(userhome), path[1:i])
442
443 return userhome + path[i:]
444 else:
445 compat_expanduser = os.path.expanduser
446
447
448if sys.version_info < (3, 0):
449 def compat_print(s):
450 from .utils import preferredencoding
451 print(s.encode(preferredencoding(), 'xmlcharrefreplace'))
452else:
453 def compat_print(s):
b061ea6e 454 assert isinstance(s, compat_str)
8c25f81b
PH
455 print(s)
456
457
458try:
459 subprocess_check_output = subprocess.check_output
460except AttributeError:
461 def subprocess_check_output(*args, **kwargs):
462 assert 'input' not in kwargs
463 p = subprocess.Popen(*args, stdout=subprocess.PIPE, **kwargs)
464 output, _ = p.communicate()
465 ret = p.poll()
466 if ret:
467 raise subprocess.CalledProcessError(ret, p.args, output=output)
468 return output
469
470if sys.version_info < (3, 0) and sys.platform == 'win32':
471 def compat_getpass(prompt, *args, **kwargs):
472 if isinstance(prompt, compat_str):
baa70803 473 from .utils import preferredencoding
8c25f81b
PH
474 prompt = prompt.encode(preferredencoding())
475 return getpass.getpass(prompt, *args, **kwargs)
476else:
477 compat_getpass = getpass.getpass
478
614db89a 479# Python < 2.6.5 require kwargs to be bytes
c7b0add8 480try:
c6973bd4
PH
481 def _testfunc(x):
482 pass
483 _testfunc(**{'x': 0})
c7b0add8
PH
484except TypeError:
485 def compat_kwargs(kwargs):
486 return dict((bytes(k), v) for k, v in kwargs.items())
487else:
488 compat_kwargs = lambda kwargs: kwargs
8c25f81b 489
e07e9313 490
be4a824d
PH
491if sys.version_info < (2, 7):
492 def compat_socket_create_connection(address, timeout, source_address=None):
493 host, port = address
494 err = None
495 for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM):
496 af, socktype, proto, canonname, sa = res
497 sock = None
498 try:
499 sock = socket.socket(af, socktype, proto)
500 sock.settimeout(timeout)
501 if source_address:
502 sock.bind(source_address)
503 sock.connect(sa)
504 return sock
505 except socket.error as _:
506 err = _
507 if sock is not None:
508 sock.close()
509 if err is not None:
510 raise err
511 else:
611c1dd9 512 raise socket.error('getaddrinfo returns an empty list')
be4a824d
PH
513else:
514 compat_socket_create_connection = socket.create_connection
515
516
e07e9313
PH
517# Fix https://github.com/rg3/youtube-dl/issues/4223
518# See http://bugs.python.org/issue9161 for what is broken
519def workaround_optparse_bug9161():
07e378fa
PH
520 op = optparse.OptionParser()
521 og = optparse.OptionGroup(op, 'foo')
e07e9313 522 try:
07e378fa 523 og.add_option('-t')
b244b5c3 524 except TypeError:
e07e9313
PH
525 real_add_option = optparse.OptionGroup.add_option
526
527 def _compat_add_option(self, *args, **kwargs):
528 enc = lambda v: (
529 v.encode('ascii', 'replace') if isinstance(v, compat_str)
530 else v)
531 bargs = [enc(a) for a in args]
532 bkwargs = dict(
533 (k, enc(v)) for k, v in kwargs.items())
534 return real_add_option(self, *bargs, **bkwargs)
535 optparse.OptionGroup.add_option = _compat_add_option
536
003c69a8
JMF
537if hasattr(shutil, 'get_terminal_size'): # Python >= 3.3
538 compat_get_terminal_size = shutil.get_terminal_size
539else:
540 _terminal_size = collections.namedtuple('terminal_size', ['columns', 'lines'])
541
13118a50 542 def compat_get_terminal_size(fallback=(80, 24)):
4810c48d 543 columns = compat_getenv('COLUMNS')
003c69a8
JMF
544 if columns:
545 columns = int(columns)
546 else:
547 columns = None
4810c48d 548 lines = compat_getenv('LINES')
003c69a8
JMF
549 if lines:
550 lines = int(lines)
551 else:
552 lines = None
553
4810c48d 554 if columns is None or lines is None or columns <= 0 or lines <= 0:
13118a50
YCH
555 try:
556 sp = subprocess.Popen(
557 ['stty', 'size'],
558 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
559 out, err = sp.communicate()
f2dbc540 560 _lines, _columns = map(int, out.split())
13118a50
YCH
561 except Exception:
562 _columns, _lines = _terminal_size(*fallback)
563
4810c48d 564 if columns is None or columns <= 0:
13118a50 565 columns = _columns
4810c48d 566 if lines is None or lines <= 0:
13118a50 567 lines = _lines
003c69a8
JMF
568 return _terminal_size(columns, lines)
569
a0e060ac
YCH
570try:
571 itertools.count(start=0, step=1)
572 compat_itertools_count = itertools.count
573except TypeError: # Python 2.6
574 def compat_itertools_count(start=0, step=1):
575 n = start
576 while True:
577 yield n
578 n += step
e07e9313 579
67134eab
JMF
580if sys.version_info >= (3, 0):
581 from tokenize import tokenize as compat_tokenize_tokenize
582else:
583 from tokenize import generate_tokens as compat_tokenize_tokenize
e07e9313 584
8c25f81b 585__all__ = [
8bb56eee 586 'compat_HTMLParser',
8c25f81b 587 'compat_HTTPError',
0196149c 588 'compat_basestring',
8c25f81b
PH
589 'compat_chr',
590 'compat_cookiejar',
799207e8 591 'compat_cookies',
36e6f62c 592 'compat_etree_fromstring',
8c25f81b 593 'compat_expanduser',
003c69a8 594 'compat_get_terminal_size',
8c25f81b
PH
595 'compat_getenv',
596 'compat_getpass',
597 'compat_html_entities',
8c25f81b 598 'compat_http_client',
83fda3c0 599 'compat_http_server',
a0e060ac 600 'compat_itertools_count',
c7b0add8 601 'compat_kwargs',
8c25f81b 602 'compat_ord',
e9c0cdd3 603 'compat_os_name',
8c25f81b
PH
604 'compat_parse_qs',
605 'compat_print',
51f579b6 606 'compat_shlex_split',
be4a824d 607 'compat_socket_create_connection',
987493ae 608 'compat_str',
8c25f81b 609 'compat_subprocess_get_DEVNULL',
67134eab 610 'compat_tokenize_tokenize',
8c25f81b
PH
611 'compat_urllib_error',
612 'compat_urllib_parse',
613 'compat_urllib_parse_unquote',
aa99aa4e 614 'compat_urllib_parse_unquote_plus',
9fefc886 615 'compat_urllib_parse_unquote_to_bytes',
15707c7e 616 'compat_urllib_parse_urlencode',
8c25f81b
PH
617 'compat_urllib_parse_urlparse',
618 'compat_urllib_request',
0a67a363
YCH
619 'compat_urllib_request_DataHandler',
620 'compat_urllib_response',
8c25f81b
PH
621 'compat_urlparse',
622 'compat_urlretrieve',
623 'compat_xml_parse_error',
57f7e3c6 624 'compat_xpath',
8c25f81b
PH
625 'shlex_quote',
626 'subprocess_check_output',
e07e9313 627 'workaround_optparse_bug9161',
8c25f81b 628]