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