animeondemand.py (12536B)
1 from __future__ import unicode_literals 2 3 import re 4 5 from .common import InfoExtractor 6 from ..compat import compat_str 7 from ..utils import ( 8 determine_ext, 9 extract_attributes, 10 ExtractorError, 11 url_or_none, 12 urlencode_postdata, 13 urljoin, 14 ) 15 16 17 class AnimeOnDemandIE(InfoExtractor): 18 _VALID_URL = r'https?://(?:www\.)?anime-on-demand\.de/anime/(?P<id>\d+)' 19 _LOGIN_URL = 'https://www.anime-on-demand.de/users/sign_in' 20 _APPLY_HTML5_URL = 'https://www.anime-on-demand.de/html5apply' 21 _NETRC_MACHINE = 'animeondemand' 22 # German-speaking countries of Europe 23 _GEO_COUNTRIES = ['AT', 'CH', 'DE', 'LI', 'LU'] 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 = urljoin(self._LOGIN_URL, post_url) 80 81 response = self._download_webpage( 82 post_url, None, 'Logging in', 83 data=urlencode_postdata(login_form), headers={ 84 'Referer': self._LOGIN_URL, 85 }) 86 87 if all(p not in response for p in ('>Logout<', 'href="/users/sign_out"')): 88 error = self._search_regex( 89 r'<p[^>]+\bclass=(["\'])(?:(?!\1).)*\balert\b(?:(?!\1).)*\1[^>]*>(?P<error>.+?)</p>', 90 response, 'error', default=None, group='error') 91 if error: 92 raise ExtractorError('Unable to login: %s' % error, expected=True) 93 raise ExtractorError('Unable to log in') 94 95 def _real_initialize(self): 96 self._login() 97 98 def _real_extract(self, url): 99 anime_id = self._match_id(url) 100 101 webpage = self._download_webpage(url, anime_id) 102 103 if 'data-playlist=' not in webpage: 104 self._download_webpage( 105 self._APPLY_HTML5_URL, anime_id, 106 'Activating HTML5 beta', 'Unable to apply HTML5 beta') 107 webpage = self._download_webpage(url, anime_id) 108 109 csrf_token = self._html_search_meta( 110 'csrf-token', webpage, 'csrf token', fatal=True) 111 112 anime_title = self._html_search_regex( 113 r'(?s)<h1[^>]+itemprop="name"[^>]*>(.+?)</h1>', 114 webpage, 'anime name') 115 anime_description = self._html_search_regex( 116 r'(?s)<div[^>]+itemprop="description"[^>]*>(.+?)</div>', 117 webpage, 'anime description', default=None) 118 119 def extract_info(html, video_id, num=None): 120 title, description = [None] * 2 121 formats = [] 122 123 for input_ in re.findall( 124 r'<input[^>]+class=["\'].*?streamstarter[^>]+>', html): 125 attributes = extract_attributes(input_) 126 title = attributes.get('data-dialog-header') 127 playlist_urls = [] 128 for playlist_key in ('data-playlist', 'data-otherplaylist', 'data-stream'): 129 playlist_url = attributes.get(playlist_key) 130 if isinstance(playlist_url, compat_str) and re.match( 131 r'/?[\da-zA-Z]+', playlist_url): 132 playlist_urls.append(attributes[playlist_key]) 133 if not playlist_urls: 134 continue 135 136 lang = attributes.get('data-lang') 137 lang_note = attributes.get('value') 138 139 for playlist_url in playlist_urls: 140 kind = self._search_regex( 141 r'videomaterialurl/\d+/([^/]+)/', 142 playlist_url, 'media kind', default=None) 143 format_id_list = [] 144 if lang: 145 format_id_list.append(lang) 146 if kind: 147 format_id_list.append(kind) 148 if not format_id_list and num is not None: 149 format_id_list.append(compat_str(num)) 150 format_id = '-'.join(format_id_list) 151 format_note = ', '.join(filter(None, (kind, lang_note))) 152 item_id_list = [] 153 if format_id: 154 item_id_list.append(format_id) 155 item_id_list.append('videomaterial') 156 playlist = self._download_json( 157 urljoin(url, playlist_url), video_id, 158 'Downloading %s JSON' % ' '.join(item_id_list), 159 headers={ 160 'X-Requested-With': 'XMLHttpRequest', 161 'X-CSRF-Token': csrf_token, 162 'Referer': url, 163 'Accept': 'application/json, text/javascript, */*; q=0.01', 164 }, fatal=False) 165 if not playlist: 166 continue 167 stream_url = url_or_none(playlist.get('streamurl')) 168 if stream_url: 169 rtmp = re.search( 170 r'^(?P<url>rtmpe?://(?P<host>[^/]+)/(?P<app>.+/))(?P<playpath>mp[34]:.+)', 171 stream_url) 172 if rtmp: 173 formats.append({ 174 'url': rtmp.group('url'), 175 'app': rtmp.group('app'), 176 'play_path': rtmp.group('playpath'), 177 'page_url': url, 178 'player_url': 'https://www.anime-on-demand.de/assets/jwplayer.flash-55abfb34080700304d49125ce9ffb4a6.swf', 179 'rtmp_real_time': True, 180 'format_id': 'rtmp', 181 'ext': 'flv', 182 }) 183 continue 184 start_video = playlist.get('startvideo', 0) 185 playlist = playlist.get('playlist') 186 if not playlist or not isinstance(playlist, list): 187 continue 188 playlist = playlist[start_video] 189 title = playlist.get('title') 190 if not title: 191 continue 192 description = playlist.get('description') 193 for source in playlist.get('sources', []): 194 file_ = source.get('file') 195 if not file_: 196 continue 197 ext = determine_ext(file_) 198 format_id_list = [lang, kind] 199 if ext == 'm3u8': 200 format_id_list.append('hls') 201 elif source.get('type') == 'video/dash' or ext == 'mpd': 202 format_id_list.append('dash') 203 format_id = '-'.join(filter(None, format_id_list)) 204 if ext == 'm3u8': 205 file_formats = self._extract_m3u8_formats( 206 file_, video_id, 'mp4', 207 entry_protocol='m3u8_native', m3u8_id=format_id, fatal=False) 208 elif source.get('type') == 'video/dash' or ext == 'mpd': 209 continue 210 file_formats = self._extract_mpd_formats( 211 file_, video_id, mpd_id=format_id, fatal=False) 212 else: 213 continue 214 for f in file_formats: 215 f.update({ 216 'language': lang, 217 'format_note': format_note, 218 }) 219 formats.extend(file_formats) 220 221 return { 222 'title': title, 223 'description': description, 224 'formats': formats, 225 } 226 227 def extract_entries(html, video_id, common_info, num=None): 228 info = extract_info(html, video_id, num) 229 230 if info['formats']: 231 self._sort_formats(info['formats']) 232 f = common_info.copy() 233 f.update(info) 234 yield f 235 236 # Extract teaser/trailer only when full episode is not available 237 if not info['formats']: 238 m = re.search( 239 r'data-dialog-header=(["\'])(?P<title>.+?)\1[^>]+href=(["\'])(?P<href>.+?)\3[^>]*>(?P<kind>Teaser|Trailer)<', 240 html) 241 if m: 242 f = common_info.copy() 243 f.update({ 244 'id': '%s-%s' % (f['id'], m.group('kind').lower()), 245 'title': m.group('title'), 246 'url': urljoin(url, m.group('href')), 247 }) 248 yield f 249 250 def extract_episodes(html): 251 for num, episode_html in enumerate(re.findall( 252 r'(?s)<h3[^>]+class="episodebox-title".+?>Episodeninhalt<', html), 1): 253 episodebox_title = self._search_regex( 254 (r'class="episodebox-title"[^>]+title=(["\'])(?P<title>.+?)\1', 255 r'class="episodebox-title"[^>]+>(?P<title>.+?)<'), 256 episode_html, 'episodebox title', default=None, group='title') 257 if not episodebox_title: 258 continue 259 260 episode_number = int(self._search_regex( 261 r'(?:Episode|Film)\s*(\d+)', 262 episodebox_title, 'episode number', default=num)) 263 episode_title = self._search_regex( 264 r'(?:Episode|Film)\s*\d+\s*-\s*(.+)', 265 episodebox_title, 'episode title', default=None) 266 267 video_id = 'episode-%d' % episode_number 268 269 common_info = { 270 'id': video_id, 271 'series': anime_title, 272 'episode': episode_title, 273 'episode_number': episode_number, 274 } 275 276 for e in extract_entries(episode_html, video_id, common_info): 277 yield e 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 for e in extract_entries(html, video_id, common_info): 286 yield e 287 288 def entries(): 289 has_episodes = False 290 for e in extract_episodes(webpage): 291 has_episodes = True 292 yield e 293 294 if not has_episodes: 295 for e in extract_film(webpage, anime_id): 296 yield e 297 298 return self.playlist_result( 299 entries(), anime_id, anime_title, anime_description)