[animeondemand] Add support for flash videos (closes #9944)
[youtube-dl] / youtube_dl / extractor / animeondemand.py
1 from __future__ import unicode_literals
2
3 import re
4
5 from .common import InfoExtractor
6 from ..compat import (
7     compat_urlparse,
8     compat_str,
9 )
10 from ..utils import (
11     determine_ext,
12     extract_attributes,
13     ExtractorError,
14     sanitized_Request,
15     urlencode_postdata,
16 )
17
18
19 class AnimeOnDemandIE(InfoExtractor):
20     _VALID_URL = r'https?://(?:www\.)?anime-on-demand\.de/anime/(?P<id>\d+)'
21     _LOGIN_URL = 'https://www.anime-on-demand.de/users/sign_in'
22     _APPLY_HTML5_URL = 'https://www.anime-on-demand.de/html5apply'
23     _NETRC_MACHINE = 'animeondemand'
24     _TESTS = [{
25         # jap, OmU
26         'url': 'https://www.anime-on-demand.de/anime/161',
27         'info_dict': {
28             'id': '161',
29             'title': 'Grimgar, Ashes and Illusions (OmU)',
30             'description': 'md5:6681ce3c07c7189d255ac6ab23812d31',
31         },
32         'playlist_mincount': 4,
33     }, {
34         # Film wording is used instead of Episode, ger/jap, Dub/OmU
35         'url': 'https://www.anime-on-demand.de/anime/39',
36         'only_matching': True,
37     }, {
38         # Episodes without titles, jap, OmU
39         'url': 'https://www.anime-on-demand.de/anime/162',
40         'only_matching': True,
41     }, {
42         # ger/jap, Dub/OmU, account required
43         'url': 'https://www.anime-on-demand.de/anime/169',
44         'only_matching': True,
45     }, {
46         # Full length film, non-series, ger/jap, Dub/OmU, account required
47         'url': 'https://www.anime-on-demand.de/anime/185',
48         'only_matching': True,
49     }, {
50         # Flash videos
51         'url': 'https://www.anime-on-demand.de/anime/12',
52         'only_matching': True,
53     }]
54
55     def _login(self):
56         (username, password) = self._get_login_info()
57         if username is None:
58             return
59
60         login_page = self._download_webpage(
61             self._LOGIN_URL, None, 'Downloading login page')
62
63         if '>Our licensing terms allow the distribution of animes only to German-speaking countries of Europe' in login_page:
64             self.raise_geo_restricted(
65                 '%s is only available in German-speaking countries of Europe' % self.IE_NAME)
66
67         login_form = self._form_hidden_inputs('new_user', login_page)
68
69         login_form.update({
70             'user[login]': username,
71             'user[password]': password,
72         })
73
74         post_url = self._search_regex(
75             r'<form[^>]+action=(["\'])(?P<url>.+?)\1', login_page,
76             'post url', default=self._LOGIN_URL, group='url')
77
78         if not post_url.startswith('http'):
79             post_url = compat_urlparse.urljoin(self._LOGIN_URL, post_url)
80
81         request = sanitized_Request(
82             post_url, urlencode_postdata(login_form))
83         request.add_header('Referer', self._LOGIN_URL)
84
85         response = self._download_webpage(
86             request, None, 'Logging in as %s' % username)
87
88         if all(p not in response for p in ('>Logout<', 'href="/users/sign_out"')):
89             error = self._search_regex(
90                 r'<p class="alert alert-danger">(.+?)</p>',
91                 response, 'error', default=None)
92             if error:
93                 raise ExtractorError('Unable to login: %s' % error, expected=True)
94             raise ExtractorError('Unable to log in')
95
96     def _real_initialize(self):
97         self._login()
98
99     def _real_extract(self, url):
100         anime_id = self._match_id(url)
101
102         webpage = self._download_webpage(url, anime_id)
103
104         if 'data-playlist=' not in webpage:
105             self._download_webpage(
106                 self._APPLY_HTML5_URL, anime_id,
107                 'Activating HTML5 beta', 'Unable to apply HTML5 beta')
108             webpage = self._download_webpage(url, anime_id)
109
110         csrf_token = self._html_search_meta(
111             'csrf-token', webpage, 'csrf token', fatal=True)
112
113         anime_title = self._html_search_regex(
114             r'(?s)<h1[^>]+itemprop="name"[^>]*>(.+?)</h1>',
115             webpage, 'anime name')
116         anime_description = self._html_search_regex(
117             r'(?s)<div[^>]+itemprop="description"[^>]*>(.+?)</div>',
118             webpage, 'anime description', default=None)
119
120         entries = []
121
122         def extract_info(html, video_id, num=None):
123             title, description = [None] * 2
124             formats = []
125
126             for input_ in re.findall(
127                     r'<input[^>]+class=["\'].*?streamstarter[^>]+>', html):
128                 attributes = extract_attributes(input_)
129                 title = attributes.get('data-dialog-header')
130                 playlist_urls = []
131                 for playlist_key in ('data-playlist', 'data-otherplaylist', 'data-stream'):
132                     playlist_url = attributes.get(playlist_key)
133                     if isinstance(playlist_url, compat_str) and re.match(
134                             r'/?[\da-zA-Z]+', playlist_url):
135                         playlist_urls.append(attributes[playlist_key])
136                 if not playlist_urls:
137                     continue
138
139                 lang = attributes.get('data-lang')
140                 lang_note = attributes.get('value')
141
142                 for playlist_url in playlist_urls:
143                     kind = self._search_regex(
144                         r'videomaterialurl/\d+/([^/]+)/',
145                         playlist_url, 'media kind', default=None)
146                     format_id_list = []
147                     if lang:
148                         format_id_list.append(lang)
149                     if kind:
150                         format_id_list.append(kind)
151                     if not format_id_list and num is not None:
152                         format_id_list.append(compat_str(num))
153                     format_id = '-'.join(format_id_list)
154                     format_note = ', '.join(filter(None, (kind, lang_note)))
155                     request = sanitized_Request(
156                         compat_urlparse.urljoin(url, playlist_url),
157                         headers={
158                             'X-Requested-With': 'XMLHttpRequest',
159                             'X-CSRF-Token': csrf_token,
160                             'Referer': url,
161                             'Accept': 'application/json, text/javascript, */*; q=0.01',
162                         })
163                     playlist = self._download_json(
164                         request, video_id, 'Downloading %s playlist JSON' % format_id,
165                         fatal=False)
166                     if not playlist:
167                         continue
168                     stream_url = playlist.get('streamurl')
169                     if stream_url:
170                         rtmp = re.search(
171                             r'^(?P<url>rtmpe?://(?P<host>[^/]+)/(?P<app>.+/))(?P<playpath>mp[34]:.+)',
172                             stream_url)
173                         if rtmp:
174                             formats.append({
175                                 'url': rtmp.group('url'),
176                                 'app': rtmp.group('app'),
177                                 'play_path': rtmp.group('playpath'),
178                                 'page_url': url,
179                                 'player_url': 'https://www.anime-on-demand.de/assets/jwplayer.flash-55abfb34080700304d49125ce9ffb4a6.swf',
180                                 'rtmp_real_time': True,
181                                 'format_id': 'rtmp',
182                                 'ext': 'flv',
183                             })
184                             continue
185                     start_video = playlist.get('startvideo', 0)
186                     playlist = playlist.get('playlist')
187                     if not playlist or not isinstance(playlist, list):
188                         continue
189                     playlist = playlist[start_video]
190                     title = playlist.get('title')
191                     if not title:
192                         continue
193                     description = playlist.get('description')
194                     for source in playlist.get('sources', []):
195                         file_ = source.get('file')
196                         if not file_:
197                             continue
198                         ext = determine_ext(file_)
199                         format_id_list = [lang, kind]
200                         if ext == 'm3u8':
201                             format_id_list.append('hls')
202                         elif source.get('type') == 'video/dash' or ext == 'mpd':
203                             format_id_list.append('dash')
204                         format_id = '-'.join(filter(None, format_id_list))
205                         if ext == 'm3u8':
206                             file_formats = self._extract_m3u8_formats(
207                                 file_, video_id, 'mp4',
208                                 entry_protocol='m3u8_native', m3u8_id=format_id, fatal=False)
209                         elif source.get('type') == 'video/dash' or ext == 'mpd':
210                             continue
211                             file_formats = self._extract_mpd_formats(
212                                 file_, video_id, mpd_id=format_id, fatal=False)
213                         else:
214                             continue
215                         for f in file_formats:
216                             f.update({
217                                 'language': lang,
218                                 'format_note': format_note,
219                             })
220                         formats.extend(file_formats)
221
222             return {
223                 'title': title,
224                 'description': description,
225                 'formats': formats,
226             }
227
228         def extract_entries(html, video_id, common_info, num=None):
229             info = extract_info(html, video_id, num)
230
231             if info['formats']:
232                 self._sort_formats(info['formats'])
233                 f = common_info.copy()
234                 f.update(info)
235                 entries.append(f)
236
237             # Extract teaser/trailer only when full episode is not available
238             if not info['formats']:
239                 m = re.search(
240                     r'data-dialog-header=(["\'])(?P<title>.+?)\1[^>]+href=(["\'])(?P<href>.+?)\3[^>]*>(?P<kind>Teaser|Trailer)<',
241                     html)
242                 if m:
243                     f = common_info.copy()
244                     f.update({
245                         'id': '%s-%s' % (f['id'], m.group('kind').lower()),
246                         'title': m.group('title'),
247                         'url': compat_urlparse.urljoin(url, m.group('href')),
248                     })
249                     entries.append(f)
250
251         def extract_episodes(html):
252             for num, episode_html in enumerate(re.findall(
253                     r'(?s)<h3[^>]+class="episodebox-title".+?>Episodeninhalt<', html), 1):
254                 episodebox_title = self._search_regex(
255                     (r'class="episodebox-title"[^>]+title=(["\'])(?P<title>.+?)\1',
256                      r'class="episodebox-title"[^>]+>(?P<title>.+?)<'),
257                     episode_html, 'episodebox title', default=None, group='title')
258                 if not episodebox_title:
259                     continue
260
261                 episode_number = int(self._search_regex(
262                     r'(?:Episode|Film)\s*(\d+)',
263                     episodebox_title, 'episode number', default=num))
264                 episode_title = self._search_regex(
265                     r'(?:Episode|Film)\s*\d+\s*-\s*(.+)',
266                     episodebox_title, 'episode title', default=None)
267
268                 video_id = 'episode-%d' % episode_number
269
270                 common_info = {
271                     'id': video_id,
272                     'series': anime_title,
273                     'episode': episode_title,
274                     'episode_number': episode_number,
275                 }
276
277                 extract_entries(episode_html, video_id, common_info)
278
279         def extract_film(html, video_id):
280             common_info = {
281                 'id': anime_id,
282                 'title': anime_title,
283                 'description': anime_description,
284             }
285             extract_entries(html, video_id, common_info)
286
287         extract_episodes(webpage)
288
289         if not entries:
290             extract_film(webpage, anime_id)
291
292         return self.playlist_result(entries, anime_id, anime_title, anime_description)