]>
Commit | Line | Data |
---|---|---|
1 | # coding: utf-8 | |
2 | from __future__ import unicode_literals | |
3 | ||
4 | from .common import InfoExtractor | |
5 | from ..utils import ( | |
6 | clean_html, | |
7 | determine_ext, | |
8 | encode_dict, | |
9 | int_or_none, | |
10 | sanitized_Request, | |
11 | ExtractorError, | |
12 | urlencode_postdata | |
13 | ) | |
14 | ||
15 | ||
16 | class FunimationIE(InfoExtractor): | |
17 | _VALID_URL = r'https?://(?:www\.)?funimation\.com/shows/[^/]+/videos/(?:official|promotional)/(?P<id>[^/?#&]+)' | |
18 | ||
19 | _NETRC_MACHINE = 'funimation' | |
20 | ||
21 | _TESTS = [{ | |
22 | 'url': 'http://www.funimation.com/shows/air/videos/official/breeze', | |
23 | 'info_dict': { | |
24 | 'id': '658', | |
25 | 'display_id': 'breeze', | |
26 | 'ext': 'mp4', | |
27 | 'title': 'Air - 1 - Breeze', | |
28 | 'description': 'md5:1769f43cd5fc130ace8fd87232207892', | |
29 | 'thumbnail': 're:https?://.*\.jpg', | |
30 | }, | |
31 | }, { | |
32 | 'url': 'http://www.funimation.com/shows/hacksign/videos/official/role-play', | |
33 | 'info_dict': { | |
34 | 'id': '31128', | |
35 | 'display_id': 'role-play', | |
36 | 'ext': 'mp4', | |
37 | 'title': '.hack//SIGN - 1 - Role Play', | |
38 | 'description': 'md5:b602bdc15eef4c9bbb201bb6e6a4a2dd', | |
39 | 'thumbnail': 're:https?://.*\.jpg', | |
40 | }, | |
41 | }, { | |
42 | 'url': 'http://www.funimation.com/shows/attack-on-titan-junior-high/videos/promotional/broadcast-dub-preview', | |
43 | 'info_dict': { | |
44 | 'id': '9635', | |
45 | 'display_id': 'broadcast-dub-preview', | |
46 | 'ext': 'mp4', | |
47 | 'title': 'Attack on Titan: Junior High - Broadcast Dub Preview', | |
48 | 'description': 'md5:f8ec49c0aff702a7832cd81b8a44f803', | |
49 | 'thumbnail': 're:https?://.*\.(?:jpg|png)', | |
50 | }, | |
51 | }] | |
52 | ||
53 | def _login(self): | |
54 | (username, password) = self._get_login_info() | |
55 | if username is None: | |
56 | return | |
57 | data = urlencode_postdata(encode_dict({ | |
58 | 'email_field': username, | |
59 | 'password_field': password, | |
60 | })) | |
61 | login_request = sanitized_Request('http://www.funimation.com/login', data, headers={ | |
62 | 'User-Agent': 'Mozilla/5.0 (Windows NT 5.2; WOW64; rv:42.0) Gecko/20100101 Firefox/42.0', | |
63 | 'Content-Type': 'application/x-www-form-urlencoded' | |
64 | }) | |
65 | login_page = self._download_webpage( | |
66 | login_request, None, 'Logging in as %s' % username) | |
67 | if any(p in login_page for p in ('funimation.com/logout', '>Log Out<')): | |
68 | return | |
69 | error = self._html_search_regex( | |
70 | r'(?s)<div[^>]+id=["\']errorMessages["\'][^>]*>(.+?)</div>', | |
71 | login_page, 'error messages', default=None) | |
72 | if error: | |
73 | raise ExtractorError('Unable to login: %s' % error, expected=True) | |
74 | raise ExtractorError('Unable to log in') | |
75 | ||
76 | def _real_initialize(self): | |
77 | self._login() | |
78 | ||
79 | def _real_extract(self, url): | |
80 | display_id = self._match_id(url) | |
81 | ||
82 | errors = [] | |
83 | formats = [] | |
84 | ||
85 | ERRORS_MAP = { | |
86 | 'ERROR_MATURE_CONTENT_LOGGED_IN': 'matureContentLoggedIn', | |
87 | 'ERROR_MATURE_CONTENT_LOGGED_OUT': 'matureContentLoggedOut', | |
88 | 'ERROR_SUBSCRIPTION_LOGGED_OUT': 'subscriptionLoggedOut', | |
89 | 'ERROR_VIDEO_EXPIRED': 'videoExpired', | |
90 | 'ERROR_TERRITORY_UNAVAILABLE': 'territoryUnavailable', | |
91 | 'SVODBASIC_SUBSCRIPTION_IN_PLAYER': 'basicSubscription', | |
92 | 'SVODNON_SUBSCRIPTION_IN_PLAYER': 'nonSubscription', | |
93 | 'ERROR_PLAYER_NOT_RESPONDING': 'playerNotResponding', | |
94 | 'ERROR_UNABLE_TO_CONNECT_TO_CDN': 'unableToConnectToCDN', | |
95 | 'ERROR_STREAM_NOT_FOUND': 'streamNotFound', | |
96 | } | |
97 | ||
98 | USER_AGENTS = ( | |
99 | # PC UA is served with m3u8 that provides some bonus lower quality formats | |
100 | ('pc', 'Mozilla/5.0 (Windows NT 5.2; WOW64; rv:42.0) Gecko/20100101 Firefox/42.0'), | |
101 | # Mobile UA allows to extract direct links and also does not fail when | |
102 | # PC UA fails with hulu error (e.g. | |
103 | # http://www.funimation.com/shows/hacksign/videos/official/role-play) | |
104 | ('mobile', 'Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.114 Mobile Safari/537.36'), | |
105 | ) | |
106 | ||
107 | for kind, user_agent in USER_AGENTS: | |
108 | request = sanitized_Request(url) | |
109 | request.add_header('User-Agent', user_agent) | |
110 | webpage = self._download_webpage( | |
111 | request, display_id, 'Downloading %s webpage' % kind) | |
112 | ||
113 | playlist = self._parse_json( | |
114 | self._search_regex( | |
115 | r'var\s+playersData\s*=\s*(\[.+?\]);\n', | |
116 | webpage, 'players data'), | |
117 | display_id)[0]['playlist'] | |
118 | ||
119 | items = next(item['items'] for item in playlist if item.get('items')) | |
120 | item = next(item for item in items if item.get('itemAK') == display_id) | |
121 | ||
122 | error_messages = {} | |
123 | video_error_messages = self._search_regex( | |
124 | r'var\s+videoErrorMessages\s*=\s*({.+?});\n', | |
125 | webpage, 'error messages', default=None) | |
126 | if video_error_messages: | |
127 | error_messages_json = self._parse_json(video_error_messages, display_id, fatal=False) | |
128 | if error_messages_json: | |
129 | for _, error in error_messages_json.items(): | |
130 | type_ = error.get('type') | |
131 | description = error.get('description') | |
132 | content = error.get('content') | |
133 | if type_ == 'text' and description and content: | |
134 | error_message = ERRORS_MAP.get(description) | |
135 | if error_message: | |
136 | error_messages[error_message] = content | |
137 | ||
138 | for video in item.get('videoSet', []): | |
139 | auth_token = video.get('authToken') | |
140 | if not auth_token: | |
141 | continue | |
142 | funimation_id = video.get('FUNImationID') or video.get('videoId') | |
143 | preference = 1 if video.get('languageMode') == 'dub' else 0 | |
144 | if not auth_token.startswith('?'): | |
145 | auth_token = '?%s' % auth_token | |
146 | for quality, height in (('sd', 480), ('hd', 720), ('hd1080', 1080)): | |
147 | format_url = video.get('%sUrl' % quality) | |
148 | if not format_url: | |
149 | continue | |
150 | if not format_url.startswith(('http', '//')): | |
151 | errors.append(format_url) | |
152 | continue | |
153 | if determine_ext(format_url) == 'm3u8': | |
154 | formats.extend(self._extract_m3u8_formats( | |
155 | format_url + auth_token, display_id, 'mp4', entry_protocol='m3u8_native', | |
156 | preference=preference, m3u8_id='%s-hls' % funimation_id, fatal=False)) | |
157 | else: | |
158 | tbr = int_or_none(self._search_regex( | |
159 | r'-(\d+)[Kk]', format_url, 'tbr', default=None)) | |
160 | formats.append({ | |
161 | 'url': format_url + auth_token, | |
162 | 'format_id': '%s-http-%dp' % (funimation_id, height), | |
163 | 'height': height, | |
164 | 'tbr': tbr, | |
165 | 'preference': preference, | |
166 | }) | |
167 | ||
168 | if not formats and errors: | |
169 | raise ExtractorError( | |
170 | '%s returned error: %s' | |
171 | % (self.IE_NAME, clean_html(error_messages.get(errors[0], errors[0]))), | |
172 | expected=True) | |
173 | ||
174 | self._sort_formats(formats) | |
175 | ||
176 | title = item['title'] | |
177 | artist = item.get('artist') | |
178 | if artist: | |
179 | title = '%s - %s' % (artist, title) | |
180 | description = self._og_search_description(webpage) or item.get('description') | |
181 | thumbnail = self._og_search_thumbnail(webpage) or item.get('posterUrl') | |
182 | video_id = item.get('itemId') or display_id | |
183 | ||
184 | return { | |
185 | 'id': video_id, | |
186 | 'display_id': display_id, | |
187 | 'title': title, | |
188 | 'description': description, | |
189 | 'thumbnail': thumbnail, | |
190 | 'formats': formats, | |
191 | } |