2 from __future__
import unicode_literals
8 from .common
import InfoExtractor
9 from ..compat
import compat_HTTPError
21 class FunimationIE(InfoExtractor
):
22 _VALID_URL
= r
'https?://(?:www\.)?funimation(?:\.com|now\.uk)/(?:[^/]+/)?shows/[^/]+/(?P<id>[^/?#&]+)'
24 _NETRC_MACHINE
= 'funimation'
28 'url': 'https://www.funimation.com/shows/hacksign/role-play/',
31 'display_id': 'role-play',
33 'title': '.hack//SIGN - Role Play',
34 'description': 'md5:b602bdc15eef4c9bbb201bb6e6a4a2dd',
35 'thumbnail': r
're:https?://.*\.jpg',
39 'skip_download': True,
42 'url': 'https://www.funimation.com/shows/attack-on-titan-junior-high/broadcast-dub-preview/',
45 'display_id': 'broadcast-dub-preview',
47 'title': 'Attack on Titan: Junior High - Broadcast Dub Preview',
48 'thumbnail': r
're:https?://.*\.(?:jpg|png)',
52 'skip_download': True,
55 'url': 'https://www.funimationnow.uk/shows/puzzle-dragons-x/drop-impact/simulcast/',
56 'only_matching': True,
59 'url': 'https://www.funimation.com/en/shows/hacksign/role-play/',
60 'only_matching': True,
64 username
, password
= self
._get
_login
_info
()
68 data
= self
._download
_json
(
69 'https://prod-api-funimationnow.dadcdigital.com/api/auth/login/',
70 None, 'Logging in', data
=urlencode_postdata({
74 self
._TOKEN
= data
['token']
75 except ExtractorError
as e
:
76 if isinstance(e
.cause
, compat_HTTPError
) and e
.cause
.code
== 401:
77 error
= self
._parse
_json
(e
.cause
.read().decode(), None)['error']
78 raise ExtractorError(error
, expected
=True)
81 def _real_initialize(self
):
84 def _real_extract(self
, url
):
85 display_id
= self
._match
_id
(url
)
86 webpage
= self
._download
_webpage
(url
, display_id
)
88 def _search_kane(name
):
89 return self
._search
_regex
(
90 r
"KANE_customdimensions\.%s\s*=\s*'([^']+)';" % name
,
91 webpage
, name
, default
=None)
93 title_data
= self
._parse
_json
(self
._search
_regex
(
94 r
'TITLE_DATA\s*=\s*({[^}]+})',
95 webpage
, 'title data', default
=''),
96 display_id
, js_to_json
, fatal
=False) or {}
98 video_id
= title_data
.get('id') or self
._search
_regex
([
99 r
"KANE_customdimensions.videoID\s*=\s*'(\d+)';",
100 r
'<iframe[^>]+src="/player/(\d+)',
101 ], webpage
, 'video_id', default
=None)
103 player_url
= self
._html
_search
_meta
([
106 'og:video:secure_url',
107 ], webpage
, fatal
=True)
108 video_id
= self
._search
_regex
(r
'/player/(\d+)', player_url
, 'video id')
110 title
= episode
= title_data
.get('title') or _search_kane('videoTitle') or self
._og
_search
_title
(webpage
)
111 series
= _search_kane('showName')
113 title
= '%s - %s' % (series
, title
)
114 description
= self
._html
_search
_meta
(['description', 'og:description'], webpage
, fatal
=True)
115 subtitles
= self
.extract_subtitles(url
, video_id
, display_id
)
120 headers
['Authorization'] = 'Token %s' % self
._TOKEN
121 sources
= self
._download
_json
(
122 'https://www.funimation.com/api/showexperience/%s/' % video_id
,
123 video_id
, headers
=headers
, query
={
124 'pinst_id': ''.join([random
.choice(string
.digits
+ string
.ascii_letters
) for _
in range(8)]),
126 except ExtractorError
as e
:
127 if isinstance(e
.cause
, compat_HTTPError
) and e
.cause
.code
== 403:
128 error
= self
._parse
_json
(e
.cause
.read(), video_id
)['errors'][0]
129 raise ExtractorError('%s said: %s' % (
130 self
.IE_NAME
, error
.get('detail') or error
.get('title')), expected
=True)
134 for source
in sources
:
135 source_url
= source
.get('src')
138 source_type
= source
.get('videoType') or determine_ext(source_url
)
139 if source_type
== 'm3u8':
140 formats
.extend(self
._extract
_m
3u8_formats
(
141 source_url
, video_id
, 'mp4',
142 m3u8_id
='hls', fatal
=False))
145 'format_id': source_type
,
148 self
._sort
_formats
(formats
)
152 'display_id': display_id
,
154 'description': description
,
155 'thumbnail': self
._og
_search
_thumbnail
(webpage
),
157 'season_number': int_or_none(title_data
.get('seasonNum') or _search_kane('season')),
158 'episode_number': int_or_none(title_data
.get('episodeNum')),
160 'subtitles': subtitles
,
161 'season_id': title_data
.get('seriesId'),
165 def _get_subtitles(self
, url
, video_id
, display_id
):
166 player_url
= urljoin(url
, '/player/' + video_id
)
167 player_page
= self
._download
_webpage
(player_url
, display_id
)
168 text_tracks_json_string
= self
._search
_regex
(
169 r
'"textTracks": (\[{.+?}\])',
170 player_page
, 'subtitles data', default
='')
171 text_tracks
= self
._parse
_json
(
172 text_tracks_json_string
, display_id
, js_to_json
, fatal
=False) or []
174 for text_track
in text_tracks
:
175 url_element
= {'url': text_track.get('src')}
176 language
= text_track
.get('language')
177 if text_track
.get('type') == 'CC':
179 subtitles
.setdefault(language
, []).append(url_element
)
183 class FunimationShowIE(FunimationIE
):
184 IE_NAME
= 'funimation:show'
185 _VALID_URL
= r
'(?P<url>https?://(?:www\.)?funimation(?:\.com|now\.uk)/(?P<locale>[^/]+)?/?shows/(?P<id>[^/?#&]+))/?(?:[?#]|$)'
188 'url': 'https://www.funimation.com/en/shows/sk8-the-infinity',
191 'title': 'SK8 the Infinity'
193 'playlist_count': 13,
195 'skip_download': True,
199 'url': 'https://www.funimation.com/shows/ouran-high-school-host-club/',
202 'title': 'Ouran High School Host Club'
204 'playlist_count': 26,
206 'skip_download': True,
210 def _real_extract(self
, url
):
211 base_url
, locale
, display_id
= re
.match(self
._VALID
_URL
, url
).groups()
213 show_info
= self
._download
_json
(
214 'https://title-api.prd.funimationsvc.com/v2/shows/%s?region=US&deviceType=web&locale=%s'
215 % (display_id
, locale
or 'en'), display_id
)
216 items
= self
._download
_json
(
217 'https://prod-api-funimationnow.dadcdigital.com/api/funimation/episodes/?limit=99999&title_id=%s'
218 % show_info
.get('id'), display_id
).get('items')
219 vod_items
= map(lambda k
: dict_get(k
, ('mostRecentSvod', 'mostRecentAvod')).get('item'), items
)
223 'id': show_info
['id'],
224 'title': show_info
['name'],
227 '%s/%s' % (base_url
, vod_item
.get('episodeSlug')), FunimationIE
.ie_key(),
228 vod_item
.get('episodeId'), vod_item
.get('episodeName'))
229 for vod_item
in sorted(vod_items
, key
=lambda x
: x
.get('episodeOrder'))],