Taylor Fox Dahlin commited on
Commit
af9a117
·
unverified ·
1 Parent(s): c19b467

Added new exception for region-locked videos. (#861)

Browse files
pytube/__main__.py CHANGED
@@ -25,6 +25,7 @@ from pytube.exceptions import MembersOnly
25
  from pytube.exceptions import RecordingUnavailable
26
  from pytube.exceptions import VideoUnavailable
27
  from pytube.exceptions import VideoPrivate
 
28
  from pytube.extract import apply_descrambler
29
  from pytube.extract import apply_signature
30
  from pytube.extract import get_ytplayer_config
@@ -113,6 +114,7 @@ class YouTube:
113
  raise VideoUnavailable(video_id=self.video_id)
114
 
115
  status, messages = extract.playability_status(self.watch_html)
 
116
  for reason in messages:
117
  if status == 'UNPLAYABLE':
118
  if reason == (
@@ -123,6 +125,9 @@ class YouTube:
123
  elif reason == 'This live stream recording is not available.':
124
  raise RecordingUnavailable(video_id=self.video_id)
125
  else:
 
 
 
126
  raise VideoUnavailable(video_id=self.video_id)
127
  elif status == 'LOGIN_REQUIRED':
128
  if reason == (
 
25
  from pytube.exceptions import RecordingUnavailable
26
  from pytube.exceptions import VideoUnavailable
27
  from pytube.exceptions import VideoPrivate
28
+ from pytube.exceptions import VideoRegionBlocked
29
  from pytube.extract import apply_descrambler
30
  from pytube.extract import apply_signature
31
  from pytube.extract import get_ytplayer_config
 
114
  raise VideoUnavailable(video_id=self.video_id)
115
 
116
  status, messages = extract.playability_status(self.watch_html)
117
+
118
  for reason in messages:
119
  if status == 'UNPLAYABLE':
120
  if reason == (
 
125
  elif reason == 'This live stream recording is not available.':
126
  raise RecordingUnavailable(video_id=self.video_id)
127
  else:
128
+ if reason == 'Video unavailable':
129
+ if extract.is_region_blocked(self.watch_html):
130
+ raise VideoRegionBlocked(video_id=self.video_id)
131
  raise VideoUnavailable(video_id=self.video_id)
132
  elif status == 'LOGIN_REQUIRED':
133
  if reason == (
pytube/exceptions.py CHANGED
@@ -96,5 +96,17 @@ class MembersOnly(PytubeError):
96
  self.video_id = video_id
97
 
98
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  class HTMLParseError(PytubeError):
100
  """HTML could not be parsed"""
 
96
  self.video_id = video_id
97
 
98
 
99
+ class VideoRegionBlocked(ExtractError):
100
+ def __init__(self, video_id: str):
101
+ """
102
+ :param str video_id:
103
+ A YouTube video identifier.
104
+ """
105
+ super().__init__(
106
+ '%s is not available in your region' % video_id
107
+ )
108
+ self.video_id = video_id
109
+
110
+
111
  class HTMLParseError(PytubeError):
112
  """HTML could not be parsed"""
pytube/extract.py CHANGED
@@ -100,6 +100,34 @@ def is_age_restricted(watch_html: str) -> bool:
100
  return True
101
 
102
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  def playability_status(watch_html: str) -> (str, str):
104
  """Return the playability status and status explanation of a video.
105
 
@@ -114,7 +142,7 @@ def playability_status(watch_html: str) -> (str, str):
114
  :returns:
115
  Playability status and reason of the video.
116
  """
117
- player_response = json.loads(initial_player_response(watch_html))
118
  status_dict = player_response.get('playabilityStatus', {})
119
  if 'status' in status_dict:
120
  if 'reason' in status_dict:
@@ -467,7 +495,7 @@ def initial_data(watch_html: str) -> str:
467
  except HTMLParseError:
468
  pass
469
 
470
- raise RegexMatchError(caller="initial_data", pattern='initial_data_pattern')
471
 
472
 
473
  def initial_player_response(watch_html: str) -> str:
@@ -479,11 +507,20 @@ def initial_player_response(watch_html: str) -> str:
479
  @param watch_html: Html of the watch page
480
  @return:
481
  """
482
- pattern = r"window\[['\"]ytInitialPlayerResponse['\"]]\s*=\s*({[^\n]+});"
483
- try:
484
- return regex_search(pattern, watch_html, 1)
485
- except RegexMatchError:
486
- return "{}"
 
 
 
 
 
 
 
 
 
487
 
488
 
489
  def metadata(initial_data) -> Optional[YouTubeMetadata]:
 
100
  return True
101
 
102
 
103
+ def is_region_blocked(watch_html: str) -> bool:
104
+ """Determine if a video is not available in the user's region.
105
+
106
+ :param str watch_html:
107
+ The html contents of the watch page.
108
+ :rtype: bool
109
+ :returns:
110
+ True if the video is blocked in the users region.
111
+ False if not, or if unknown.
112
+ """
113
+ player_response = initial_player_response(watch_html)
114
+ country_code_patterns = [
115
+ r"gl\s*=\s*['\"](\w{2})['\"]", # gl="US"
116
+ r"['\"]gl['\"]\s*:\s*['\"](\w{2})['\"]" # "gl":"US"
117
+ ]
118
+ for pattern in country_code_patterns:
119
+ try:
120
+ yt_detected_country = regex_search(pattern, watch_html, 1)
121
+ available_countries = player_response[
122
+ 'microformat']['playerMicroformatRenderer']['availableCountries']
123
+ except (KeyError, RegexMatchError):
124
+ pass
125
+ else:
126
+ if yt_detected_country not in available_countries:
127
+ return True
128
+ return False
129
+
130
+
131
  def playability_status(watch_html: str) -> (str, str):
132
  """Return the playability status and status explanation of a video.
133
 
 
142
  :returns:
143
  Playability status and reason of the video.
144
  """
145
+ player_response = initial_player_response(watch_html)
146
  status_dict = player_response.get('playabilityStatus', {})
147
  if 'status' in status_dict:
148
  if 'reason' in status_dict:
 
495
  except HTMLParseError:
496
  pass
497
 
498
+ raise RegexMatchError(caller='initial_data', pattern='initial_data_pattern')
499
 
500
 
501
  def initial_player_response(watch_html: str) -> str:
 
507
  @param watch_html: Html of the watch page
508
  @return:
509
  """
510
+ patterns = [
511
+ r"window\[['\"]ytInitialPlayerResponse['\"]]\s*=\s*",
512
+ r"ytInitialPlayerResponse\s*=\s*"
513
+ ]
514
+ for pattern in patterns:
515
+ try:
516
+ return parse_for_object(watch_html, pattern)
517
+ except HTMLParseError:
518
+ pass
519
+
520
+ raise RegexMatchError(
521
+ caller='initial_player_response',
522
+ pattern='initial_player_response_pattern'
523
+ )
524
 
525
 
526
  def metadata(initial_data) -> Optional[YouTubeMetadata]:
tests/conftest.py CHANGED
@@ -72,6 +72,13 @@ def missing_recording():
72
  return load_playback_file(filename)
73
 
74
 
 
 
 
 
 
 
 
75
  @pytest.fixture
76
  def playlist_html():
77
  """Youtube playlist HTML loaded on 2020-01-25 from
 
72
  return load_playback_file(filename)
73
 
74
 
75
+ @pytest.fixture
76
+ def region_blocked():
77
+ """Youtube instance initialized with video id hZpzr8TbF08."""
78
+ filename = "yt-video-hZpzr8TbF08-html.json.gz"
79
+ return load_playback_file(filename)
80
+
81
+
82
  @pytest.fixture
83
  def playlist_html():
84
  """Youtube playlist HTML loaded on 2020-01-25 from
tests/mocks/yt-video-hZpzr8TbF08-html.json.gz ADDED
Binary file (96.7 kB). View file
 
tests/test_exceptions.py CHANGED
@@ -8,6 +8,7 @@ from pytube.exceptions import RecordingUnavailable
8
  from pytube.exceptions import RegexMatchError
9
  from pytube.exceptions import VideoUnavailable
10
  from pytube.exceptions import VideoPrivate
 
11
 
12
 
13
  def test_video_unavailable():
@@ -27,18 +28,18 @@ def test_regex_match_error():
27
 
28
  def test_live_stream_error():
29
  try:
30
- raise LiveStreamError(video_id="YLnZklYFe7E")
31
  except LiveStreamError as e:
32
- assert e.video_id == "YLnZklYFe7E" # noqa: PT017
33
- assert str(e) == "YLnZklYFe7E is streaming live and cannot be loaded"
34
 
35
 
36
- def test_recording_unavailable():
37
  try:
38
- raise RecordingUnavailable(video_id="5YceQ8YqYMc")
39
  except RecordingUnavailable as e:
40
- assert e.video_id == "5YceQ8YqYMc" # noqa: PT017
41
- assert str(e) == "5YceQ8YqYMc does not have a live stream recording available"
42
 
43
 
44
  def test_private_error():
@@ -49,6 +50,14 @@ def test_private_error():
49
  assert str(e) == 'm8uHb5jIGN8 is a private video'
50
 
51
 
 
 
 
 
 
 
 
 
52
  def test_raises_video_private(private):
53
  with mock.patch('pytube.request.urlopen') as mock_url_open:
54
  # Mock the responses to YouTube
@@ -71,3 +80,15 @@ def test_raises_recording_unavailable(missing_recording):
71
  mock_url_open.return_value = mock_url_open_object
72
  with pytest.raises(RecordingUnavailable):
73
  YouTube('https://youtube.com/watch?v=5YceQ8YqYMc')
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  from pytube.exceptions import RegexMatchError
9
  from pytube.exceptions import VideoUnavailable
10
  from pytube.exceptions import VideoPrivate
11
+ from pytube.exceptions import VideoRegionBlocked
12
 
13
 
14
  def test_video_unavailable():
 
28
 
29
  def test_live_stream_error():
30
  try:
31
+ raise LiveStreamError(video_id='YLnZklYFe7E')
32
  except LiveStreamError as e:
33
+ assert e.video_id == 'YLnZklYFe7E' # noqa: PT017
34
+ assert str(e) == 'YLnZklYFe7E is streaming live and cannot be loaded'
35
 
36
 
37
+ def test_recording_unavailable_error():
38
  try:
39
+ raise RecordingUnavailable(video_id='5YceQ8YqYMc')
40
  except RecordingUnavailable as e:
41
+ assert e.video_id == '5YceQ8YqYMc' # noqa: PT017
42
+ assert str(e) == '5YceQ8YqYMc does not have a live stream recording available'
43
 
44
 
45
  def test_private_error():
 
50
  assert str(e) == 'm8uHb5jIGN8 is a private video'
51
 
52
 
53
+ def test_region_locked_error():
54
+ try:
55
+ raise VideoRegionBlocked('hZpzr8TbF08')
56
+ except VideoRegionBlocked as e:
57
+ assert e.video_id == 'hZpzr8TbF08' # noqa: PT017
58
+ assert str(e) == 'hZpzr8TbF08 is not available in your region'
59
+
60
+
61
  def test_raises_video_private(private):
62
  with mock.patch('pytube.request.urlopen') as mock_url_open:
63
  # Mock the responses to YouTube
 
80
  mock_url_open.return_value = mock_url_open_object
81
  with pytest.raises(RecordingUnavailable):
82
  YouTube('https://youtube.com/watch?v=5YceQ8YqYMc')
83
+
84
+
85
+ def test_raises_video_region_blocked(region_blocked):
86
+ with mock.patch('pytube.request.urlopen') as mock_url_open:
87
+ # Mock the responses to YouTube
88
+ mock_url_open_object = mock.Mock()
89
+ mock_url_open_object.read.side_effect = [
90
+ region_blocked['watch_html'].encode('utf-8')
91
+ ]
92
+ mock_url_open.return_value = mock_url_open_object
93
+ with pytest.raises(VideoRegionBlocked):
94
+ YouTube('https://youtube.com/watch?v=hZpzr8TbF08')
tests/test_helpers.py CHANGED
@@ -121,7 +121,7 @@ def test_create_mock_html_json(mock_url_open, mock_open):
121
  # 2. vid_info_raw
122
  # 3. js
123
  mock_url_open_object.read.side_effect = [
124
- (b'yt.setConfig({"PLAYER_CONFIG":{"args":[]}});ytInitialData = {}'
125
  b'"jsUrl":"/s/player/13371337/player_ias.vflset/en_US/base.js"'),
126
  b'vid_info_raw',
127
  b'js_result',
 
121
  # 2. vid_info_raw
122
  # 3. js
123
  mock_url_open_object.read.side_effect = [
124
+ (b'yt.setConfig({"PLAYER_CONFIG":{"args":[]}});ytInitialData = {};ytInitialPlayerResponse = {};' # noqa: E501
125
  b'"jsUrl":"/s/player/13371337/player_ias.vflset/en_US/base.js"'),
126
  b'vid_info_raw',
127
  b'js_result',