unit tests
Browse files- pytube/__main__.py +0 -2
- pytube/cipher.py +0 -3
- pytube/extract.py +0 -2
- pytube/helpers.py +0 -14
- pytube/mixins.py +5 -1
- pytube/streams.py +0 -2
- tests/conftest.py +18 -5
- tests/mocks/yt-video-9bZkp7q19f0-1507588332.json.tar.gz +0 -0
- tests/mocks/yt-video-9bZkp7q19f0.json +0 -0
- tests/mocks/yt-video-QRS8MkLhQmM-1507588031.json.tar.gz +0 -0
- tests/test_mixins.py +3 -0
- tests/test_query.py +91 -0
pytube/__main__.py
CHANGED
@@ -19,7 +19,6 @@ from pytube import Stream
|
|
19 |
from pytube import StreamQuery
|
20 |
from pytube.compat import parse_qsl
|
21 |
from pytube.helpers import apply_mixin
|
22 |
-
from pytube.helpers import memoize
|
23 |
|
24 |
|
25 |
logger = logging.getLogger(__name__)
|
@@ -159,7 +158,6 @@ class YouTube(object):
|
|
159 |
self.fmt_streams.append(video)
|
160 |
|
161 |
@property
|
162 |
-
@memoize
|
163 |
def streams(self):
|
164 |
"""Interface to query both adaptive (DASH) and progressive streams."""
|
165 |
return StreamQuery([s for s in self.fmt_streams])
|
|
|
19 |
from pytube import StreamQuery
|
20 |
from pytube.compat import parse_qsl
|
21 |
from pytube.helpers import apply_mixin
|
|
|
22 |
|
23 |
|
24 |
logger = logging.getLogger(__name__)
|
|
|
158 |
self.fmt_streams.append(video)
|
159 |
|
160 |
@property
|
|
|
161 |
def streams(self):
|
162 |
"""Interface to query both adaptive (DASH) and progressive streams."""
|
163 |
return StreamQuery([s for s in self.fmt_streams])
|
pytube/cipher.py
CHANGED
@@ -23,7 +23,6 @@ import re
|
|
23 |
from itertools import chain
|
24 |
|
25 |
from pytube.exceptions import RegexMatchError
|
26 |
-
from pytube.helpers import memoize
|
27 |
from pytube.helpers import regex_search
|
28 |
|
29 |
|
@@ -104,7 +103,6 @@ def get_transform_object(js, var):
|
|
104 |
)
|
105 |
|
106 |
|
107 |
-
@memoize
|
108 |
def get_transform_map(js, var):
|
109 |
"""Build a transform function lookup.
|
110 |
|
@@ -235,7 +233,6 @@ def parse_function(js_func):
|
|
235 |
return regex_search(pattern, js_func, groups=True)
|
236 |
|
237 |
|
238 |
-
@memoize
|
239 |
def get_signature(js, ciphered_signature):
|
240 |
"""Decipher the signature.
|
241 |
|
|
|
23 |
from itertools import chain
|
24 |
|
25 |
from pytube.exceptions import RegexMatchError
|
|
|
26 |
from pytube.helpers import regex_search
|
27 |
|
28 |
|
|
|
103 |
)
|
104 |
|
105 |
|
|
|
106 |
def get_transform_map(js, var):
|
107 |
"""Build a transform function lookup.
|
108 |
|
|
|
233 |
return regex_search(pattern, js_func, groups=True)
|
234 |
|
235 |
|
|
|
236 |
def get_signature(js, ciphered_signature):
|
237 |
"""Decipher the signature.
|
238 |
|
pytube/extract.py
CHANGED
@@ -5,7 +5,6 @@ from collections import OrderedDict
|
|
5 |
|
6 |
from pytube.compat import quote
|
7 |
from pytube.compat import urlencode
|
8 |
-
from pytube.helpers import memoize
|
9 |
from pytube.helpers import regex_search
|
10 |
|
11 |
|
@@ -88,7 +87,6 @@ def mime_type_codec(mime_type_codec):
|
|
88 |
return mime_type, [c.strip() for c in codecs.split(',')]
|
89 |
|
90 |
|
91 |
-
@memoize
|
92 |
def get_ytplayer_config(watch_html):
|
93 |
"""Get the YouTube player configuration data from the watch html.
|
94 |
|
|
|
5 |
|
6 |
from pytube.compat import quote
|
7 |
from pytube.compat import urlencode
|
|
|
8 |
from pytube.helpers import regex_search
|
9 |
|
10 |
|
|
|
87 |
return mime_type, [c.strip() for c in codecs.split(',')]
|
88 |
|
89 |
|
|
|
90 |
def get_ytplayer_config(watch_html):
|
91 |
"""Get the YouTube player configuration data from the watch html.
|
92 |
|
pytube/helpers.py
CHANGED
@@ -2,7 +2,6 @@
|
|
2 |
"""Various helper functions implemented by pytube."""
|
3 |
from __future__ import absolute_import
|
4 |
|
5 |
-
import functools
|
6 |
import logging
|
7 |
import pprint
|
8 |
import re
|
@@ -88,16 +87,3 @@ def safe_filename(s, max_length=255):
|
|
88 |
regex = re.compile(pattern, re.UNICODE)
|
89 |
filename = regex.sub('', s)
|
90 |
return filename[:max_length].rsplit(' ', 0)[0]
|
91 |
-
|
92 |
-
|
93 |
-
def memoize(func):
|
94 |
-
"""Decorate that caches input arguments for return values."""
|
95 |
-
cache = func.cache = {}
|
96 |
-
|
97 |
-
@functools.wraps(func)
|
98 |
-
def wrapper(*args, **kwargs):
|
99 |
-
key = str(args) + str(kwargs)
|
100 |
-
if key not in cache:
|
101 |
-
cache[key] = func(*args, **kwargs)
|
102 |
-
return cache[key]
|
103 |
-
return wrapper
|
|
|
2 |
"""Various helper functions implemented by pytube."""
|
3 |
from __future__ import absolute_import
|
4 |
|
|
|
5 |
import logging
|
6 |
import pprint
|
7 |
import re
|
|
|
87 |
regex = re.compile(pattern, re.UNICODE)
|
88 |
filename = regex.sub('', s)
|
89 |
return filename[:max_length].rsplit(' ', 0)[0]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pytube/mixins.py
CHANGED
@@ -34,6 +34,7 @@ def apply_signature(config_args, fmt, js):
|
|
34 |
# For certain videos, YouTube will just provide them pre-signed, in
|
35 |
# which case there's no real magic to download them and we can skip
|
36 |
# the whole signature decrambling entirely.
|
|
|
37 |
continue
|
38 |
|
39 |
signature = cipher.get_signature(js, stream['s'])
|
@@ -63,4 +64,7 @@ def apply_descrambler(stream_data, key):
|
|
63 |
{k: unquote(v) for k, v in parse_qsl(i)}
|
64 |
for i in stream_data[key].split(',')
|
65 |
]
|
66 |
-
logger.debug(
|
|
|
|
|
|
|
|
34 |
# For certain videos, YouTube will just provide them pre-signed, in
|
35 |
# which case there's no real magic to download them and we can skip
|
36 |
# the whole signature decrambling entirely.
|
37 |
+
logger.debug('signature found, skip decipher')
|
38 |
continue
|
39 |
|
40 |
signature = cipher.get_signature(js, stream['s'])
|
|
|
64 |
{k: unquote(v) for k, v in parse_qsl(i)}
|
65 |
for i in stream_data[key].split(',')
|
66 |
]
|
67 |
+
logger.debug(
|
68 |
+
'applying descrambler\n%s',
|
69 |
+
pprint.pformat(stream_data[key], indent=2),
|
70 |
+
)
|
pytube/streams.py
CHANGED
@@ -15,7 +15,6 @@ import pprint
|
|
15 |
|
16 |
from pytube import extract
|
17 |
from pytube import request
|
18 |
-
from pytube.helpers import memoize
|
19 |
from pytube.helpers import safe_filename
|
20 |
from pytube.itags import get_format_profile
|
21 |
|
@@ -133,7 +132,6 @@ class Stream(object):
|
|
133 |
return video, audio
|
134 |
|
135 |
@property
|
136 |
-
@memoize
|
137 |
def filesize(self):
|
138 |
"""File size of the media stream in bytes."""
|
139 |
headers = request.get(self.url, headers=True)
|
|
|
15 |
|
16 |
from pytube import extract
|
17 |
from pytube import request
|
|
|
18 |
from pytube.helpers import safe_filename
|
19 |
from pytube.itags import get_format_profile
|
20 |
|
|
|
132 |
return video, audio
|
133 |
|
134 |
@property
|
|
|
135 |
def filesize(self):
|
136 |
"""File size of the media stream in bytes."""
|
137 |
headers = request.get(self.url, headers=True)
|
tests/conftest.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1 |
# -*- coding: utf-8 -*-
|
2 |
"""Reusable dependency injected testing components."""
|
|
|
3 |
import json
|
4 |
import os
|
5 |
|
@@ -8,13 +9,11 @@ import pytest
|
|
8 |
from pytube import YouTube
|
9 |
|
10 |
|
11 |
-
|
12 |
-
def gangnam_style():
|
13 |
-
"""Youtube instance initialized with video id 9bZkp7q19f0."""
|
14 |
cur_dir = os.path.dirname(os.path.realpath(__file__))
|
15 |
-
fp = os.path.join(cur_dir, 'mocks',
|
16 |
video = None
|
17 |
-
with open(fp, '
|
18 |
video = json.loads(fh.read())
|
19 |
yt = YouTube(
|
20 |
url='https://www.youtube.com/watch?v=9bZkp7q19f0',
|
@@ -25,3 +24,17 @@ def gangnam_style():
|
|
25 |
yt.vid_info = video['video_info']
|
26 |
yt.init()
|
27 |
return yt
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
# -*- coding: utf-8 -*-
|
2 |
"""Reusable dependency injected testing components."""
|
3 |
+
import gzip
|
4 |
import json
|
5 |
import os
|
6 |
|
|
|
9 |
from pytube import YouTube
|
10 |
|
11 |
|
12 |
+
def load_from_playback_file(filename):
|
|
|
|
|
13 |
cur_dir = os.path.dirname(os.path.realpath(__file__))
|
14 |
+
fp = os.path.join(cur_dir, 'mocks', filename)
|
15 |
video = None
|
16 |
+
with gzip.open(fp, 'rb') as fh:
|
17 |
video = json.loads(fh.read())
|
18 |
yt = YouTube(
|
19 |
url='https://www.youtube.com/watch?v=9bZkp7q19f0',
|
|
|
24 |
yt.vid_info = video['video_info']
|
25 |
yt.init()
|
26 |
return yt
|
27 |
+
|
28 |
+
|
29 |
+
@pytest.fixture
|
30 |
+
def gangnam_style():
|
31 |
+
"""Youtube instance initialized with video id 9bZkp7q19f0."""
|
32 |
+
filename = 'yt-video-9bZkp7q19f0-1507588332.json.tar.gz'
|
33 |
+
return load_from_playback_file(filename)
|
34 |
+
|
35 |
+
|
36 |
+
@pytest.fixture
|
37 |
+
def youtube_captions_and_subtitles():
|
38 |
+
"""Youtube instance initialized with video id QRS8MkLhQmM."""
|
39 |
+
filename = 'yt-video-QRS8MkLhQmM-1507588031.json.tar.gz'
|
40 |
+
return load_from_playback_file(filename)
|
tests/mocks/yt-video-9bZkp7q19f0-1507588332.json.tar.gz
ADDED
Binary file (483 kB). View file
|
|
tests/mocks/yt-video-9bZkp7q19f0.json
DELETED
The diff for this file is too large to render.
See raw diff
|
|
tests/mocks/yt-video-QRS8MkLhQmM-1507588031.json.tar.gz
ADDED
Binary file (367 kB). View file
|
|
tests/test_mixins.py
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
# -*- coding: utf-8 -*-
|
2 |
+
def test_pre_signed_video(youtube_captions_and_subtitles):
|
3 |
+
assert youtube_captions_and_subtitles.streams.count() == 15
|
tests/test_query.py
ADDED
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# -*- coding: utf-8 -*-
|
2 |
+
"""Unit tests for the :class:`StreamQuery <StreamQuery>` class."""
|
3 |
+
import pytest
|
4 |
+
|
5 |
+
|
6 |
+
def test_count(gangnam_style):
|
7 |
+
assert gangnam_style.streams.count() == 22
|
8 |
+
|
9 |
+
|
10 |
+
@pytest.mark.parametrize(
|
11 |
+
'test_input,expected', [
|
12 |
+
({'progressive': True}, ['22', '43', '18', '36', '17']),
|
13 |
+
({'resolution': '720p'}, ['22', '136', '247']),
|
14 |
+
({'res': '720p'}, ['22', '136', '247']),
|
15 |
+
({'fps': 30, 'resolution': '480p'}, ['135', '244']),
|
16 |
+
({'mime_type': 'audio/mp4'}, ['140']),
|
17 |
+
({'type': 'audio'}, ['140', '171', '249', '250', '251']),
|
18 |
+
({'subtype': '3gpp'}, ['36', '17']),
|
19 |
+
({'abr': '128kbps'}, ['43', '140', '171']),
|
20 |
+
({'bitrate': '128kbps'}, ['43', '140', '171']),
|
21 |
+
({'audio_codec': 'vorbis'}, ['43', '171']),
|
22 |
+
({'video_codec': 'vp9'}, ['248', '247', '244', '243', '242', '278']),
|
23 |
+
({'only_audio': True}, ['140', '171', '249', '250', '251']),
|
24 |
+
({'only_video': True, 'video_codec': 'avc1.4d4015'}, ['133']),
|
25 |
+
({'progressive': True}, ['22', '43', '18', '36', '17']),
|
26 |
+
({'adaptive': True, 'resolution': '1080p'}, ['137', '248']),
|
27 |
+
({'custom_filter_functions': [lambda s: s.itag == '22']}, ['22']),
|
28 |
+
],
|
29 |
+
)
|
30 |
+
def test_filters(test_input, expected, gangnam_style):
|
31 |
+
result = [s.itag for s in gangnam_style.streams.filter(**test_input).all()]
|
32 |
+
assert result == expected
|
33 |
+
|
34 |
+
|
35 |
+
def test_get_last(gangnam_style):
|
36 |
+
assert gangnam_style.streams.last().itag == '251'
|
37 |
+
|
38 |
+
|
39 |
+
def test_get_last_empty_results(gangnam_style):
|
40 |
+
assert not gangnam_style.streams.filter(video_codec='vp20').last()
|
41 |
+
|
42 |
+
|
43 |
+
def test_get_first(gangnam_style):
|
44 |
+
assert gangnam_style.streams.first().itag == '22'
|
45 |
+
|
46 |
+
|
47 |
+
def test_get_first_empty_results(gangnam_style):
|
48 |
+
assert not gangnam_style.streams.filter(video_codec='vp20').first()
|
49 |
+
|
50 |
+
|
51 |
+
def test_order_by(gangnam_style):
|
52 |
+
itags = [
|
53 |
+
s.itag for s in gangnam_style.streams
|
54 |
+
.filter(progressive=True)
|
55 |
+
.order_by('itag')
|
56 |
+
.all()
|
57 |
+
]
|
58 |
+
|
59 |
+
assert itags == ['17', '18', '22', '36', '43']
|
60 |
+
|
61 |
+
|
62 |
+
def test_order_by_descending(gangnam_style):
|
63 |
+
itags = [
|
64 |
+
s.itag for s in gangnam_style.streams
|
65 |
+
.filter(progressive=True)
|
66 |
+
.order_by('itag')
|
67 |
+
.desc()
|
68 |
+
.all()
|
69 |
+
]
|
70 |
+
|
71 |
+
assert itags == ['43', '36', '22', '18', '17']
|
72 |
+
|
73 |
+
|
74 |
+
def test_order_by_ascending(gangnam_style):
|
75 |
+
itags = [
|
76 |
+
s.itag for s in gangnam_style.streams
|
77 |
+
.filter(progressive=True)
|
78 |
+
.order_by('itag')
|
79 |
+
.asc()
|
80 |
+
.all()
|
81 |
+
]
|
82 |
+
|
83 |
+
assert itags == ['17', '18', '22', '36', '43']
|
84 |
+
|
85 |
+
|
86 |
+
def test_get_by_itag(gangnam_style):
|
87 |
+
assert gangnam_style.streams.get_by_itag(22).itag == '22'
|
88 |
+
|
89 |
+
|
90 |
+
def test_get_by_non_existent_itag(gangnam_style):
|
91 |
+
assert not gangnam_style.streams.get_by_itag(22983)
|