]>
Commit | Line | Data |
---|---|---|
48fbb100 AMW |
1 | # coding: utf-8 |
2 | from __future__ import unicode_literals | |
3 | ||
4 | import re | |
71155991 | 5 | import json |
48fbb100 AMW |
6 | |
7 | from .common import InfoExtractor | |
71155991 | 8 | from ..utils import ( |
9 | ExtractorError, | |
da4d4191 PH |
10 | xpath_text, |
11 | float_or_none, | |
71155991 | 12 | ) |
48fbb100 | 13 | |
5f6a1245 | 14 | |
48fbb100 | 15 | class AdultSwimIE(InfoExtractor): |
71155991 | 16 | _VALID_URL = r'https?://(?:www\.)?adultswim\.com/videos/(?P<is_playlist>playlists/)?(?P<show_path>[^/]+)/(?P<episode_path>[^/?#]+)/?' |
17 | ||
18 | _TESTS = [{ | |
19 | 'url': 'http://adultswim.com/videos/rick-and-morty/pilot', | |
d415299a | 20 | 'playlist': [ |
158f8cad | 21 | { |
71155991 | 22 | 'md5': '247572debc75c7652f253c8daa51a14d', |
d415299a | 23 | 'info_dict': { |
71155991 | 24 | 'id': 'rQxZvXQ4ROaSOqq-or2Mow-0', |
d415299a | 25 | 'ext': 'flv', |
71155991 | 26 | 'title': 'Rick and Morty - Pilot Part 1', |
27 | 'description': "Rick moves in with his daughter's family and establishes himself as a bad influence on his grandson, Morty. " | |
28 | }, | |
d415299a AMW |
29 | }, |
30 | { | |
71155991 | 31 | 'md5': '77b0e037a4b20ec6b98671c4c379f48d', |
d415299a | 32 | 'info_dict': { |
71155991 | 33 | 'id': 'rQxZvXQ4ROaSOqq-or2Mow-3', |
d415299a | 34 | 'ext': 'flv', |
71155991 | 35 | 'title': 'Rick and Morty - Pilot Part 4', |
36 | 'description': "Rick moves in with his daughter's family and establishes himself as a bad influence on his grandson, Morty. " | |
37 | }, | |
d415299a | 38 | }, |
71155991 | 39 | ], |
40 | 'info_dict': { | |
41 | 'title': 'Rick and Morty - Pilot', | |
42 | 'description': "Rick moves in with his daughter's family and establishes himself as a bad influence on his grandson, Morty. " | |
43 | } | |
44 | }, { | |
45 | 'url': 'http://www.adultswim.com/videos/playlists/american-parenting/putting-francine-out-of-business/', | |
46 | 'playlist': [ | |
d415299a | 47 | { |
71155991 | 48 | 'md5': '2eb5c06d0f9a1539da3718d897f13ec5', |
d415299a | 49 | 'info_dict': { |
71155991 | 50 | 'id': '-t8CamQlQ2aYZ49ItZCFog-0', |
d415299a | 51 | 'ext': 'flv', |
71155991 | 52 | 'title': 'American Dad - Putting Francine Out of Business', |
53 | 'description': 'Stan hatches a plan to get Francine out of the real estate business.Watch more American Dad on [adult swim].' | |
54 | }, | |
d415299a | 55 | } |
71155991 | 56 | ], |
57 | 'info_dict': { | |
58 | 'title': 'American Dad - Putting Francine Out of Business', | |
59 | 'description': 'Stan hatches a plan to get Francine out of the real estate business.Watch more American Dad on [adult swim].' | |
60 | }, | |
61 | }] | |
62 | ||
63 | @staticmethod | |
64 | def find_video_info(collection, slug): | |
65 | for video in collection.get('videos'): | |
66 | if video.get('slug') == slug: | |
67 | return video | |
68 | ||
69 | @staticmethod | |
70 | def find_collection_by_linkURL(collections, linkURL): | |
71 | for collection in collections: | |
72 | if collection.get('linkURL') == linkURL: | |
73 | return collection | |
74 | ||
75 | @staticmethod | |
76 | def find_collection_containing_video(collections, slug): | |
77 | for collection in collections: | |
78 | for video in collection.get('videos'): | |
79 | if video.get('slug') == slug: | |
80 | return collection, video | |
48fbb100 AMW |
81 | |
82 | def _real_extract(self, url): | |
83 | mobj = re.match(self._VALID_URL, url) | |
71155991 | 84 | show_path = mobj.group('show_path') |
85 | episode_path = mobj.group('episode_path') | |
86 | is_playlist = True if mobj.group('is_playlist') else False | |
87 | ||
88 | webpage = self._download_webpage(url, episode_path) | |
89 | ||
90 | # Extract the value of `bootstrappedData` from the Javascript in the page. | |
91 | bootstrappedDataJS = self._search_regex(r'var bootstrappedData = ({.*});', webpage, episode_path) | |
92 | ||
93 | try: | |
94 | bootstrappedData = json.loads(bootstrappedDataJS) | |
95 | except ValueError as ve: | |
96 | errmsg = '%s: Failed to parse JSON ' % episode_path | |
97 | raise ExtractorError(errmsg, cause=ve) | |
98 | ||
99 | # Downloading videos from a /videos/playlist/ URL needs to be handled differently. | |
100 | # NOTE: We are only downloading one video (the current one) not the playlist | |
101 | if is_playlist: | |
102 | collections = bootstrappedData['playlists']['collections'] | |
103 | collection = self.find_collection_by_linkURL(collections, show_path) | |
104 | video_info = self.find_video_info(collection, episode_path) | |
105 | ||
106 | show_title = video_info['showTitle'] | |
107 | segment_ids = [video_info['videoPlaybackID']] | |
108 | else: | |
109 | collections = bootstrappedData['show']['collections'] | |
110 | collection, video_info = self.find_collection_containing_video(collections, episode_path) | |
111 | ||
112 | show = bootstrappedData['show'] | |
113 | show_title = show['title'] | |
114 | segment_ids = [clip['videoPlaybackID'] for clip in video_info['clips']] | |
115 | ||
116 | episode_id = video_info['id'] | |
117 | episode_title = video_info['title'] | |
118 | episode_description = video_info['description'] | |
119 | episode_duration = video_info.get('duration') | |
48fbb100 AMW |
120 | |
121 | entries = [] | |
71155991 | 122 | for part_num, segment_id in enumerate(segment_ids): |
123 | segment_url = 'http://www.adultswim.com/videos/api/v0/assets?id=%s&platform=mobile' % segment_id | |
48fbb100 | 124 | |
71155991 | 125 | segment_title = '%s - %s' % (show_title, episode_title) |
126 | if len(segment_ids) > 1: | |
127 | segment_title += ' Part %d' % (part_num + 1) | |
48fbb100 | 128 | |
7e6a7153 PH |
129 | idoc = self._download_xml( |
130 | segment_url, segment_title, | |
131 | 'Downloading segment information', 'Unable to download segment information') | |
48fbb100 | 132 | |
da4d4191 PH |
133 | segment_duration = float_or_none( |
134 | xpath_text(idoc, './/trt', 'segment duration').strip()) | |
71155991 | 135 | |
48fbb100 AMW |
136 | formats = [] |
137 | file_els = idoc.findall('.//files/file') | |
138 | ||
139 | for file_el in file_els: | |
140 | bitrate = file_el.attrib.get('bitrate') | |
71155991 | 141 | ftype = file_el.attrib.get('type') |
142 | ||
48fbb100 | 143 | formats.append({ |
71155991 | 144 | 'format_id': '%s_%s' % (bitrate, ftype), |
145 | 'url': file_el.text.strip(), | |
d9222264 JMF |
146 | # The bitrate may not be a number (for example: 'iphone') |
147 | 'tbr': int(bitrate) if bitrate.isdigit() else None, | |
71155991 | 148 | 'quality': 1 if ftype == 'hd' else -1 |
48fbb100 AMW |
149 | }) |
150 | ||
151 | self._sort_formats(formats) | |
152 | ||
153 | entries.append({ | |
154 | 'id': segment_id, | |
155 | 'title': segment_title, | |
156 | 'formats': formats, | |
71155991 | 157 | 'duration': segment_duration, |
158 | 'description': episode_description | |
48fbb100 AMW |
159 | }) |
160 | ||
161 | return { | |
162 | '_type': 'playlist', | |
163 | 'id': episode_id, | |
71155991 | 164 | 'display_id': episode_path, |
48fbb100 | 165 | 'entries': entries, |
71155991 | 166 | 'title': '%s - %s' % (show_title, episode_title), |
167 | 'description': episode_description, | |
168 | 'duration': episode_duration | |
48fbb100 | 169 | } |