funimation.py (5828B)
1 # coding: utf-8 2 from __future__ import unicode_literals 3 4 import random 5 import string 6 7 from .common import InfoExtractor 8 from ..compat import compat_HTTPError 9 from ..utils import ( 10 determine_ext, 11 int_or_none, 12 js_to_json, 13 ExtractorError, 14 urlencode_postdata 15 ) 16 17 18 class FunimationIE(InfoExtractor): 19 _VALID_URL = r'https?://(?:www\.)?funimation(?:\.com|now\.uk)/(?:[^/]+/)?shows/[^/]+/(?P<id>[^/?#&]+)' 20 21 _NETRC_MACHINE = 'funimation' 22 _TOKEN = None 23 24 _TESTS = [{ 25 'url': 'https://www.funimation.com/shows/hacksign/role-play/', 26 'info_dict': { 27 'id': '91144', 28 'display_id': 'role-play', 29 'ext': 'mp4', 30 'title': '.hack//SIGN - Role Play', 31 'description': 'md5:b602bdc15eef4c9bbb201bb6e6a4a2dd', 32 'thumbnail': r're:https?://.*\.jpg', 33 }, 34 'params': { 35 # m3u8 download 36 'skip_download': True, 37 }, 38 }, { 39 'url': 'https://www.funimation.com/shows/attack-on-titan-junior-high/broadcast-dub-preview/', 40 'info_dict': { 41 'id': '210051', 42 'display_id': 'broadcast-dub-preview', 43 'ext': 'mp4', 44 'title': 'Attack on Titan: Junior High - Broadcast Dub Preview', 45 'thumbnail': r're:https?://.*\.(?:jpg|png)', 46 }, 47 'params': { 48 # m3u8 download 49 'skip_download': True, 50 }, 51 }, { 52 'url': 'https://www.funimationnow.uk/shows/puzzle-dragons-x/drop-impact/simulcast/', 53 'only_matching': True, 54 }, { 55 # with lang code 56 'url': 'https://www.funimation.com/en/shows/hacksign/role-play/', 57 'only_matching': True, 58 }] 59 60 def _login(self): 61 username, password = self._get_login_info() 62 if username is None: 63 return 64 try: 65 data = self._download_json( 66 'https://prod-api-funimationnow.dadcdigital.com/api/auth/login/', 67 None, 'Logging in', data=urlencode_postdata({ 68 'username': username, 69 'password': password, 70 })) 71 self._TOKEN = data['token'] 72 except ExtractorError as e: 73 if isinstance(e.cause, compat_HTTPError) and e.cause.code == 401: 74 error = self._parse_json(e.cause.read().decode(), None)['error'] 75 raise ExtractorError(error, expected=True) 76 raise 77 78 def _real_initialize(self): 79 self._login() 80 81 def _real_extract(self, url): 82 display_id = self._match_id(url) 83 webpage = self._download_webpage(url, display_id) 84 85 def _search_kane(name): 86 return self._search_regex( 87 r"KANE_customdimensions\.%s\s*=\s*'([^']+)';" % name, 88 webpage, name, default=None) 89 90 title_data = self._parse_json(self._search_regex( 91 r'TITLE_DATA\s*=\s*({[^}]+})', 92 webpage, 'title data', default=''), 93 display_id, js_to_json, fatal=False) or {} 94 95 video_id = title_data.get('id') or self._search_regex([ 96 r"KANE_customdimensions.videoID\s*=\s*'(\d+)';", 97 r'<iframe[^>]+src="/player/(\d+)', 98 ], webpage, 'video_id', default=None) 99 if not video_id: 100 player_url = self._html_search_meta([ 101 'al:web:url', 102 'og:video:url', 103 'og:video:secure_url', 104 ], webpage, fatal=True) 105 video_id = self._search_regex(r'/player/(\d+)', player_url, 'video id') 106 107 title = episode = title_data.get('title') or _search_kane('videoTitle') or self._og_search_title(webpage) 108 series = _search_kane('showName') 109 if series: 110 title = '%s - %s' % (series, title) 111 description = self._html_search_meta(['description', 'og:description'], webpage, fatal=True) 112 113 try: 114 headers = {} 115 if self._TOKEN: 116 headers['Authorization'] = 'Token %s' % self._TOKEN 117 sources = self._download_json( 118 'https://www.funimation.com/api/showexperience/%s/' % video_id, 119 video_id, headers=headers, query={ 120 'pinst_id': ''.join([random.choice(string.digits + string.ascii_letters) for _ in range(8)]), 121 })['items'] 122 except ExtractorError as e: 123 if isinstance(e.cause, compat_HTTPError) and e.cause.code == 403: 124 error = self._parse_json(e.cause.read(), video_id)['errors'][0] 125 raise ExtractorError('%s said: %s' % ( 126 self.IE_NAME, error.get('detail') or error.get('title')), expected=True) 127 raise 128 129 formats = [] 130 for source in sources: 131 source_url = source.get('src') 132 if not source_url: 133 continue 134 source_type = source.get('videoType') or determine_ext(source_url) 135 if source_type == 'm3u8': 136 formats.extend(self._extract_m3u8_formats( 137 source_url, video_id, 'mp4', 138 m3u8_id='hls', fatal=False)) 139 else: 140 formats.append({ 141 'format_id': source_type, 142 'url': source_url, 143 }) 144 self._sort_formats(formats) 145 146 return { 147 'id': video_id, 148 'display_id': display_id, 149 'title': title, 150 'description': description, 151 'thumbnail': self._og_search_thumbnail(webpage), 152 'series': series, 153 'season_number': int_or_none(title_data.get('seasonNum') or _search_kane('season')), 154 'episode_number': int_or_none(title_data.get('episodeNum')), 155 'episode': episode, 156 'season_id': title_data.get('seriesId'), 157 'formats': formats, 158 }