diff --git a/test/test_reddit.py b/test/test_reddit.py new file mode 100644 index 000000000..090ebc965 --- /dev/null +++ b/test/test_reddit.py @@ -0,0 +1,70 @@ +from __future__ import unicode_literals + +import unittest + +from test.helper import FakeYDL +from youtube_dl.extractor.reddit import RedditRIE + + +class _TestRedditRIE(RedditRIE): + def __init__(self, downloader): + super(_TestRedditRIE, self).__init__(downloader) + self.cookies = {} + self.requests = [] + self.download_kwargs = None + self.skip_old_session_cookie = False + + def _get_cookies(self, url): + return self.cookies + + def _request_webpage(self, url_or_request, video_id, note=None, errnote=None, + fatal=True, data=None, headers={}, query={}, expected_status=None): + self.requests.append((url_or_request, query)) + if 'old.reddit.com' in url_or_request and not self.skip_old_session_cookie: + self.cookies['loid'] = object() + elif '/svc/shreddit/' in url_or_request: + self.cookies['loid'] = object() + return True + + def _download_json(self, url, video_id, **kwargs): + self.download_kwargs = kwargs + return [{ + 'data': { + 'children': [{ + 'data': { + 'url': 'https://v.redd.it/test-video', + }, + }], + }, + }] + + def _extract_m3u8_formats(self, *args, **kwargs): + return [] + + def _extract_mpd_formats(self, *args, **kwargs): + return [] + + def _sort_formats(self, formats): + pass + + +class TestRedditSession(unittest.TestCase): + def setUp(self): + self.ie = _TestRedditRIE(FakeYDL()) + + def test_initializes_anonymous_session(self): + result = self.ie._real_extract( + 'https://www.reddit.com/r/videos/comments/abc123/example-post/') + + self.assertEqual(result['url'], 'https://v.redd.it/test-video') + self.assertEqual(self.ie.requests[0][0], 'https://old.reddit.com/') + self.assertEqual(self.ie.download_kwargs.get('expected_status'), 403) + + def test_falls_back_to_shreddit_when_old_reddit_does_not_set_loid(self): + self.ie.skip_old_session_cookie = True + self.ie._real_extract( + 'https://www.reddit.com/r/videos/comments/abc123/example-post/') + + self.assertEqual(self.ie.requests[0][0], 'https://old.reddit.com/') + self.assertIn('/svc/shreddit/example-post', self.ie.requests[1][0]) + self.assertEqual(self.ie.requests[1][1]['render-mode'], 'partial') diff --git a/youtube_dl/extractor/reddit.py b/youtube_dl/extractor/reddit.py index 222fa0172..25c6c2ccb 100644 --- a/youtube_dl/extractor/reddit.py +++ b/youtube_dl/extractor/reddit.py @@ -50,7 +50,7 @@ class RedditIE(InfoExtractor): class RedditRIE(InfoExtractor): - _VALID_URL = r'(?Phttps?://(?:[^/]+\.)?reddit\.com/r/[^/]+/comments/(?P[^/?#&]+))' + _VALID_URL = r'(?Phttps?://(?:[^/]+\.)?reddit\.com/r/[^/]+/comments/(?P[^/?#&]+)(?:/(?P[^/?#&]+))?)' _TESTS = [{ 'url': 'https://www.reddit.com/r/videos/comments/6rrwyj/that_small_heart_attack/', 'info_dict': { @@ -97,14 +97,44 @@ class RedditRIE(InfoExtractor): 'only_matching': True, }] + @property + def _is_logged_in(self): + return bool(self._get_cookies('https://www.reddit.com/').get('reddit_session')) + + def _real_initialize(self): + if not self._is_logged_in: + # We need a 'loid' cookie to access the API anonymously. + self._request_webpage( + 'https://old.reddit.com/', None, + 'Setting up session via old reddit', 'Session request failed', fatal=False) + def _real_extract(self, url): mobj = re.match(self._VALID_URL, url) - url, video_id = mobj.group('url', 'id') + url, video_id, slug = mobj.group('url', 'id', 'slug') video_id = self._match_id(url) - data = self._download_json( - url + '/.json', video_id)[0]['data']['children'][0]['data'] + # Fallback for if old reddit session request failed. + if not self._is_logged_in and not self._get_cookies('https://www.reddit.com/').get('loid'): + self._request_webpage( + 'https://www.reddit.com/svc/shreddit/%s' % (slug or video_id), video_id, + 'Setting up session via shreddit', 'Session request failed', fatal=False, + query={ + 'seeker-session': 'false', + 'render-mode': 'partial', + 'referer': url, + }) + + try: + data = self._download_json( + url + '/.json', video_id, expected_status=403)[0]['data']['children'][0]['data'] + except ExtractorError as e: + if isinstance(getattr(e, 'cause', None), ValueError): + if self._is_logged_in: + raise ExtractorError( + 'Your IP address is unable to access the Reddit API', expected=True) + self.raise_login_required('Account authentication is required') + raise video_url = data['url']