Skip to content

Commit

Permalink
Merge commit 'ed5c44e7b74ac77f87ca5ed6cb5e964a0c6a0678'
Browse files Browse the repository at this point in the history
* commit 'ed5c44e7b74ac77f87ca5ed6cb5e964a0c6a0678':
  [compat] Replace deficient ChainMap class in Py3.3 and earlier * fix version check
  [compat] Replace deficient ChainMap class in Py3.3 and earlier
  [jsinterp] Improve try/catch/finally support
  [jsinterp] Fix bug in operator precedence * from yt-dlp/yt-dlp@164b03c * added tests
  [YouTube] Improve error check for n-sig processing
  [core] Avoid processing empty format list after removing bad formats * also ensure compat encoding of error strings
  [utils] Ensure RFC3986 encoding result is unicode
  [infoq] Avoid crash if the page has no `mp3Form`
  [uktvplay] Support domain without .uktv
  [jsinterp] Clean up and pull yt-dlp style * add compat_re_Pattern * improve compat_collections_chain_map * use class JS_Undefined * remove unused code
  [jsinterp] Handle regexp literals and throw/catch execution (ytdl-org#31182)
  [jsinterp] Improve JS language support (ytdl-org#31175)
  • Loading branch information
gaming-hacker committed Aug 26, 2022
2 parents df0ebb9 + ed5c44e commit 8951a25
Show file tree
Hide file tree
Showing 9 changed files with 523 additions and 120 deletions.
172 changes: 171 additions & 1 deletion test/test_jsinterp.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@
import unittest
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from youtube_dl.jsinterp import JSInterpreter
import math
import re

from youtube_dl.compat import compat_re_Pattern

from youtube_dl.jsinterp import JS_Undefined, JSInterpreter


class TestJSInterpreter(unittest.TestCase):
Expand Down Expand Up @@ -48,6 +53,9 @@ def test_operators(self):
jsi = JSInterpreter('function f(){return 1 << 5;}')
self.assertEqual(jsi.call_function('f'), 32)

jsi = JSInterpreter('function f(){return 2 ** 5}')
self.assertEqual(jsi.call_function('f'), 32)

jsi = JSInterpreter('function f(){return 19 & 21;}')
self.assertEqual(jsi.call_function('f'), 17)

Expand All @@ -57,6 +65,18 @@ def test_operators(self):
jsi = JSInterpreter('function f(){return []? 2+3: 4;}')
self.assertEqual(jsi.call_function('f'), 5)

jsi = JSInterpreter('function f(){return 1 == 2}')
self.assertEqual(jsi.call_function('f'), False)

jsi = JSInterpreter('function f(){return 0 && 1 || 2;}')
self.assertEqual(jsi.call_function('f'), 2)

jsi = JSInterpreter('function f(){return 0 ?? 42;}')
self.assertEqual(jsi.call_function('f'), 0)

jsi = JSInterpreter('function f(){return "life, the universe and everything" < 42;}')
self.assertFalse(jsi.call_function('f'))

def test_array_access(self):
jsi = JSInterpreter('function f(){var x = [1,2,3]; x[0] = 4; x[0] = 5; x[2.0] = 7; return x;}')
self.assertEqual(jsi.call_function('f'), [5, 2, 7])
Expand Down Expand Up @@ -175,6 +195,30 @@ def test_try(self):
''')
self.assertEqual(jsi.call_function('x'), 10)

def test_catch(self):
jsi = JSInterpreter('''
function x() { try{throw 10} catch(e){return 5} }
''')
self.assertEqual(jsi.call_function('x'), 5)

def test_finally(self):
jsi = JSInterpreter('''
function x() { try{throw 10} finally {return 42} }
''')
self.assertEqual(jsi.call_function('x'), 42)
jsi = JSInterpreter('''
function x() { try{throw 10} catch(e){return 5} finally {return 42} }
''')
self.assertEqual(jsi.call_function('x'), 42)

def test_nested_try(self):
jsi = JSInterpreter('''
function x() {try {
try{throw 10} finally {throw 42}
} catch(e){return 5} }
''')
self.assertEqual(jsi.call_function('x'), 5)

def test_for_loop_continue(self):
jsi = JSInterpreter('''
function x() { a=0; for (i=0; i-10; i++) { continue; a++ } return a }
Expand All @@ -187,6 +231,14 @@ def test_for_loop_break(self):
''')
self.assertEqual(jsi.call_function('x'), 0)

def test_for_loop_try(self):
jsi = JSInterpreter('''
function x() {
for (i=0; i-10; i++) { try { if (i == 5) throw i} catch {return 10} finally {break} };
return 42 }
''')
self.assertEqual(jsi.call_function('x'), 42)

def test_literal_list(self):
jsi = JSInterpreter('''
function x() { return [1, 2, "asdf", [5, 6, 7]][3] }
Expand All @@ -203,6 +255,11 @@ def test_comma(self):
''')
self.assertEqual(jsi.call_function('x'), 7)

jsi = JSInterpreter('''
function x() { return (l=[0,1,2,3], function(a, b){return a+b})((l[1], l[2]), l[3]) }
''')
self.assertEqual(jsi.call_function('x'), 5)

def test_void(self):
jsi = JSInterpreter('''
function x() { return void 42; }
Expand All @@ -215,6 +272,119 @@ def test_return_function(self):
''')
self.assertEqual(jsi.call_function('x')([]), 1)

def test_null(self):
jsi = JSInterpreter('''
function x() { return null; }
''')
self.assertIs(jsi.call_function('x'), None)

jsi = JSInterpreter('''
function x() { return [null > 0, null < 0, null == 0, null === 0]; }
''')
self.assertEqual(jsi.call_function('x'), [False, False, False, False])

jsi = JSInterpreter('''
function x() { return [null >= 0, null <= 0]; }
''')
self.assertEqual(jsi.call_function('x'), [True, True])

def test_undefined(self):
jsi = JSInterpreter('''
function x() { return undefined === undefined; }
''')
self.assertTrue(jsi.call_function('x'))

jsi = JSInterpreter('''
function x() { return undefined; }
''')
self.assertIs(jsi.call_function('x'), JS_Undefined)

jsi = JSInterpreter('''
function x() { let v; return v; }
''')
self.assertIs(jsi.call_function('x'), JS_Undefined)

jsi = JSInterpreter('''
function x() { return [undefined === undefined, undefined == undefined, undefined < undefined, undefined > undefined]; }
''')
self.assertEqual(jsi.call_function('x'), [True, True, False, False])

jsi = JSInterpreter('''
function x() { return [undefined === 0, undefined == 0, undefined < 0, undefined > 0]; }
''')
self.assertEqual(jsi.call_function('x'), [False, False, False, False])

jsi = JSInterpreter('''
function x() { return [undefined >= 0, undefined <= 0]; }
''')
self.assertEqual(jsi.call_function('x'), [False, False])

jsi = JSInterpreter('''
function x() { return [undefined > null, undefined < null, undefined == null, undefined === null]; }
''')
self.assertEqual(jsi.call_function('x'), [False, False, True, False])

jsi = JSInterpreter('''
function x() { return [undefined === null, undefined == null, undefined < null, undefined > null]; }
''')
self.assertEqual(jsi.call_function('x'), [False, True, False, False])

jsi = JSInterpreter('''
function x() { let v; return [42+v, v+42, v**42, 42**v, 0**v]; }
''')
for y in jsi.call_function('x'):
self.assertTrue(math.isnan(y))

jsi = JSInterpreter('''
function x() { let v; return v**0; }
''')
self.assertEqual(jsi.call_function('x'), 1)

jsi = JSInterpreter('''
function x() { let v; return [v>42, v<=42, v&&42, 42&&v]; }
''')
self.assertEqual(jsi.call_function('x'), [False, False, JS_Undefined, JS_Undefined])

jsi = JSInterpreter('function x(){return undefined ?? 42; }')
self.assertEqual(jsi.call_function('x'), 42)

def test_object(self):
jsi = JSInterpreter('''
function x() { return {}; }
''')
self.assertEqual(jsi.call_function('x'), {})

jsi = JSInterpreter('''
function x() { let a = {m1: 42, m2: 0 }; return [a["m1"], a.m2]; }
''')
self.assertEqual(jsi.call_function('x'), [42, 0])

jsi = JSInterpreter('''
function x() { let a; return a?.qq; }
''')
self.assertIs(jsi.call_function('x'), JS_Undefined)

jsi = JSInterpreter('''
function x() { let a = {m1: 42, m2: 0 }; return a?.qq; }
''')
self.assertIs(jsi.call_function('x'), JS_Undefined)

def test_regex(self):
jsi = JSInterpreter('''
function x() { let a=/,,[/,913,/](,)}/; }
''')
self.assertIs(jsi.call_function('x'), None)

jsi = JSInterpreter('''
function x() { let a=/,,[/,913,/](,)}/; return a; }
''')
self.assertIsInstance(jsi.call_function('x'), compat_re_Pattern)

jsi = JSInterpreter('''
function x() { let a=/,,[/,913,/](,)}/i; return a; }
''')
self.assertEqual(jsi.call_function('x').flags & ~re.U, re.I)


if __name__ == '__main__':
unittest.main()
15 changes: 14 additions & 1 deletion test/test_youtube_signature.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@
import re
import string

from youtube_dl.compat import compat_str, compat_urlretrieve

from test.helper import FakeYDL
from youtube_dl.extractor import YoutubeIE
from youtube_dl.jsinterp import JSInterpreter
from youtube_dl.compat import compat_str, compat_urlretrieve

_SIG_TESTS = [
(
Expand Down Expand Up @@ -102,6 +103,18 @@
'https://www.youtube.com/s/player/4c3f79c5/player_ias.vflset/en_US/base.js',
'TDCstCG66tEAO5pR9o', 'dbxNtZ14c-yWyw',
),
(
'https://www.youtube.com/s/player/c81bbb4a/player_ias.vflset/en_US/base.js',
'gre3EcLurNY2vqp94', 'Z9DfGxWP115WTg',
),
(
'https://www.youtube.com/s/player/1f7d5369/player_ias.vflset/en_US/base.js',
'batNX7sYqIJdkJ', 'IhOkL_zxbkOZBw',
),
(
'https://www.youtube.com/s/player/dc0c6770/player_ias.vflset/en_US/base.js',
'5EHDMgYLV6HPGk_Mu-kk', 'n9lUJLHbxUI0GQ',
),
]


Expand Down
12 changes: 6 additions & 6 deletions youtube_dl/YoutubeDL.py
Original file line number Diff line number Diff line change
Expand Up @@ -721,7 +721,7 @@ def prepare_filename(self, info_dict):
filename = encodeFilename(filename, True).decode(preferredencoding())
return sanitize_path(filename)
except ValueError as err:
self.report_error('Error in output template: ' + str(err) + ' (encoding: ' + repr(preferredencoding()) + ')')
self.report_error('Error in output template: ' + error_to_compat_str(err) + ' (encoding: ' + repr(preferredencoding()) + ')')
return None

def _match_entry(self, info_dict, incomplete):
Expand Down Expand Up @@ -1570,9 +1570,6 @@ def sanitize_numeric_fields(info):
else:
formats = info_dict['formats']

if not formats:
raise ExtractorError('No video formats found!')

def is_wellformed(f):
url = f.get('url')
if not url:
Expand All @@ -1585,7 +1582,10 @@ def is_wellformed(f):
return True

# Filter out malformed formats for better extraction robustness
formats = list(filter(is_wellformed, formats))
formats = list(filter(is_wellformed, formats or []))

if not formats:
raise ExtractorError('No video formats found!')

formats_dict = {}

Expand Down Expand Up @@ -2085,7 +2085,7 @@ def compatible_formats(formats):
try:
self.post_process(filename, info_dict)
except (PostProcessingError) as err:
self.report_error('postprocessing: %s' % str(err))
self.report_error('postprocessing: %s' % error_to_compat_str(err))
return
self.record_download_archive(info_dict)
# avoid possible nugatory search for further items (PR #26638)
Expand Down
27 changes: 25 additions & 2 deletions youtube_dl/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -3007,8 +3007,11 @@ def unpack(self, string):
# new class in collections
try:
from collections import ChainMap as compat_collections_chain_map
# Py3.3's ChainMap is deficient
if sys.version_info < (3, 4):
raise ImportError
except ImportError:
# Py < 3.3
# Py <= 3.3
class compat_collections_chain_map(compat_collections_abc.MutableMapping):

maps = [{}]
Expand All @@ -3026,18 +3029,34 @@ def __setitem__(self, k, v):
self.maps[0].__setitem__(k, v)
return

def __delitem__(self, k):
def __contains__(self, k):
return any((k in m) for m in self.maps)

def __delitem(self, k):
if k in self.maps[0]:
del self.maps[0][k]
return
raise KeyError(k)

def __delitem__(self, k):
self.__delitem(k)

def __iter__(self):
return itertools.chain(*reversed(self.maps))

def __len__(self):
return len(iter(self))

# to match Py3, don't del directly
def pop(self, k, *args):
if self.__contains__(k):
off = self.__getitem__(k)
self.__delitem(k)
return off
elif len(args) > 0:
return args[0]
raise KeyError(k)

def new_child(self, m=None, **kwargs):
m = m or {}
m.update(kwargs)
Expand All @@ -3048,6 +3067,9 @@ def parents(self):
return compat_collections_chain_map(*(self.maps[1:]))


# Pythons disagree on the type of a pattern (RegexObject, _sre.SRE_Pattern, Pattern, ...?)
compat_re_Pattern = type(re.compile(''))

if sys.version_info < (3, 3):
def compat_b64decode(s, *args, **kwargs):
if isinstance(s, compat_str):
Expand Down Expand Up @@ -3113,6 +3135,7 @@ def compat_ctypes_WINFUNCTYPE(*args, **kwargs):
'compat_os_name',
'compat_parse_qs',
'compat_print',
'compat_re_Pattern',
'compat_realpath',
'compat_setenv',
'compat_shlex_quote',
Expand Down
9 changes: 8 additions & 1 deletion youtube_dl/extractor/infoq.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# coding: utf-8

from __future__ import unicode_literals
from ..utils import (
ExtractorError,
)

from ..compat import (
compat_b64decode,
Expand Down Expand Up @@ -90,7 +93,11 @@ def _extract_http_video(self, webpage):
}]

def _extract_http_audio(self, webpage, video_id):
fields = self._form_hidden_inputs('mp3Form', webpage)
try:
fields = self._form_hidden_inputs('mp3Form', webpage)
except ExtractorError:
fields = {}

http_audio_url = fields.get('filename')
if not http_audio_url:
return []
Expand Down
2 changes: 1 addition & 1 deletion youtube_dl/extractor/uktvplay.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@


class UKTVPlayIE(InfoExtractor):
_VALID_URL = r'https?://uktvplay\.uktv\.co\.uk/(?:.+?\?.*?\bvideo=|([^/]+/)*watch-online/)(?P<id>\d+)'
_VALID_URL = r'https?://uktvplay\.(?:uktv\.)?co\.uk/(?:.+?\?.*?\bvideo=|([^/]+/)*watch-online/)(?P<id>\d+)'
_TESTS = [{
'url': 'https://uktvplay.uktv.co.uk/shows/world-at-war/c/200/watch-online/?video=2117008346001',
'info_dict': {
Expand Down
Loading

0 comments on commit 8951a25

Please sign in to comment.