5 from .common
import InfoExtractor
6 from ..networking
.exceptions
import HTTPError
23 class FunimationBaseIE(InfoExtractor
):
24 _NETRC_MACHINE
= 'funimation'
28 def _get_region(self
):
29 region_cookie
= self
._get
_cookies
('https://www.funimation.com').get('region')
30 region
= region_cookie
.value
if region_cookie
else self
.get_param('geo_bypass_country')
31 return region
or traverse_obj(
33 'https://geo-service.prd.funimationsvc.com/geo/v1/region/check', None, fatal
=False,
34 note
='Checking geo-location', errnote
='Unable to fetch geo-location information'),
37 def _perform_login(self
, username
, password
):
41 data
= self
._download
_json
(
42 'https://prod-api-funimationnow.dadcdigital.com/api/auth/login/',
43 None, 'Logging in', data
=urlencode_postdata({
47 FunimationBaseIE
._TOKEN
= data
['token']
48 except ExtractorError
as e
:
49 if isinstance(e
.cause
, HTTPError
) and e
.cause
.status
== 401:
50 error
= self
._parse
_json
(e
.cause
.response
.read().decode(), None)['error']
51 raise ExtractorError(error
, expected
=True)
55 class FunimationPageIE(FunimationBaseIE
):
56 IE_NAME
= 'funimation:page'
57 _VALID_URL
= r
'https?://(?:www\.)?funimation(?:\.com|now\.uk)/(?:(?P<lang>[^/]+)/)?(?:shows|v)/(?P<show>[^/]+)/(?P<episode>[^/?#&]+)'
60 'url': 'https://www.funimation.com/shows/attack-on-titan-junior-high/broadcast-dub-preview/',
64 'title': 'Broadcast Dub Preview',
65 # Other metadata is tested in FunimationIE
68 'skip_download': 'm3u8',
70 'add_ie': ['Funimation'],
73 'url': 'https://www.funimation.com/shows/hacksign/role-play/',
74 'only_matching': True,
77 'url': 'https://www.funimation.com/en/shows/hacksign/role-play/',
78 'only_matching': True,
80 'url': 'https://www.funimationnow.uk/shows/puzzle-dragons-x/drop-impact/simulcast/',
81 'only_matching': True,
83 'url': 'https://www.funimation.com/v/a-certain-scientific-railgun/super-powered-level-5',
84 'only_matching': True,
87 def _real_initialize(self
):
89 FunimationBaseIE
._REGION
= self
._get
_region
()
91 def _real_extract(self
, url
):
92 locale
, show
, episode
= self
._match
_valid
_url
(url
).group('lang', 'show', 'episode')
94 video_id
= traverse_obj(self
._download
_json
(
95 f
'https://title-api.prd.funimationsvc.com/v1/shows/{show}/episodes/{episode}',
96 f
'{show}_{episode}', query
={
98 'region': self
._REGION
,
99 'locale': locale
or 'en',
100 }), ('videoList', ..., 'id'), get_all
=False)
102 return self
.url_result(f
'https://www.funimation.com/player/{video_id}', FunimationIE
.ie_key(), video_id
)
105 class FunimationIE(FunimationBaseIE
):
106 _VALID_URL
= r
'https?://(?:www\.)?funimation\.com/player/(?P<id>\d+)'
109 'url': 'https://www.funimation.com/player/210051',
112 'display_id': 'broadcast-dub-preview',
114 'title': 'Broadcast Dub Preview',
115 'thumbnail': r
're:https?://.*\.(?:jpg|png)',
116 'episode': 'Broadcast Dub Preview',
117 'episode_id': '210050',
119 'season_id': '166038',
121 'series': 'Attack on Titan: Junior High',
126 'skip_download': 'm3u8',
129 'note': 'player_id should be extracted with the relevent compat-opt',
130 'url': 'https://www.funimation.com/player/210051',
133 'display_id': 'broadcast-dub-preview',
135 'title': 'Broadcast Dub Preview',
136 'thumbnail': r
're:https?://.*\.(?:jpg|png)',
137 'episode': 'Broadcast Dub Preview',
138 'episode_id': '210050',
140 'season_id': '166038',
142 'series': 'Attack on Titan: Junior High',
147 'skip_download': 'm3u8',
148 'compat_opts': ['seperate-video-versions'],
153 def _get_experiences(episode
):
154 for lang
, lang_data
in episode
.get('languages', {}).items():
155 for video_data
in lang_data
.values():
156 for version
, f
in video_data
.items():
157 yield lang
, version
.title(), f
159 def _get_episode(self
, webpage
, experience_id
=None, episode_id
=None, fatal
=True):
160 """ Extract the episode, season and show objects given either episode/experience id """
161 show
= self
._parse
_json
(
163 r
'show\s*=\s*({.+?})\s*;', webpage
, 'show data', fatal
=fatal
),
164 experience_id
, transform_source
=js_to_json
, fatal
=fatal
) or []
165 for season
in show
.get('seasons', []):
166 for episode
in season
.get('episodes', []):
167 if episode_id
is not None:
168 if str(episode
.get('episodePk')) == episode_id
:
169 return episode
, season
, show
171 for _
, _
, f
in self
._get
_experiences
(episode
):
172 if f
.get('experienceId') == experience_id
:
173 return episode
, season
, show
175 raise ExtractorError('Unable to find episode information')
177 self
.report_warning('Unable to find episode information')
180 def _real_extract(self
, url
):
181 initial_experience_id
= self
._match
_id
(url
)
182 webpage
= self
._download
_webpage
(
183 url
, initial_experience_id
, note
=f
'Downloading player webpage for {initial_experience_id}')
184 episode
, season
, show
= self
._get
_episode
(webpage
, experience_id
=int(initial_experience_id
))
185 episode_id
= str(episode
['episodePk'])
186 display_id
= episode
.get('slug') or episode_id
188 formats
, subtitles
, thumbnails
, duration
= [], {}, [], 0
189 requested_languages
, requested_versions
= self
._configuration
_arg
('language'), self
._configuration
_arg
('version')
190 language_preference
= qualities((requested_languages
or [''])[::-1])
191 source_preference
= qualities((requested_versions
or ['uncut', 'simulcast'])[::-1])
192 only_initial_experience
= 'seperate-video-versions' in self
.get_param('compat_opts', [])
194 for lang
, version
, fmt
in self
._get
_experiences
(episode
):
195 experience_id
= str(fmt
['experienceId'])
196 if ((only_initial_experience
and experience_id
!= initial_experience_id
)
197 or (requested_languages
and lang
.lower() not in requested_languages
)
198 or (requested_versions
and version
.lower() not in requested_versions
)):
200 thumbnails
.append({'url': fmt
.get('poster')})
201 duration
= max(duration
, fmt
.get('duration', 0))
202 format_name
= f
'{version} {lang} ({experience_id})'
203 self
.extract_subtitles(
204 subtitles
, experience_id
, display_id
=display_id
, format_name
=format_name
,
205 episode
=episode
if experience_id
== initial_experience_id
else episode_id
)
209 headers
['Authorization'] = f
'Token {self._TOKEN}'
210 page
= self
._download
_json
(
211 f
'https://www.funimation.com/api/showexperience/{experience_id}/',
212 display_id
, headers
=headers
, expected_status
=403, query
={
213 'pinst_id': ''.join(random
.choices(string
.digits
+ string
.ascii_letters
, k
=8)),
214 }, note
=f
'Downloading {format_name} JSON')
215 sources
= page
.get('items') or []
217 error
= try_get(page
, lambda x
: x
['errors'][0], dict)
219 self
.report_warning('{} said: Error {} - {}'.format(
220 self
.IE_NAME
, error
.get('code'), error
.get('detail') or error
.get('title')))
222 self
.report_warning('No sources found for format')
225 for source
in sources
:
226 source_url
= source
.get('src')
227 source_type
= source
.get('videoType') or determine_ext(source_url
)
228 if source_type
== 'm3u8':
229 current_formats
.extend(self
._extract
_m
3u8_formats
(
230 source_url
, display_id
, 'mp4', m3u8_id
='{}-{}'.format(experience_id
, 'hls'), fatal
=False,
231 note
=f
'Downloading {format_name} m3u8 information'))
233 current_formats
.append({
234 'format_id': f
'{experience_id}-{source_type}',
237 for f
in current_formats
:
238 # TODO: Convert language to code
241 'format_note': version
,
242 'source_preference': source_preference(version
.lower()),
243 'language_preference': language_preference(lang
.lower()),
245 formats
.extend(current_formats
)
246 if not formats
and (requested_languages
or requested_versions
):
247 self
.raise_no_formats(
248 'There are no video formats matching the requested languages/versions', expected
=True, video_id
=display_id
)
249 self
._remove
_duplicate
_formats
(formats
)
253 '_old_archive_ids': [make_archive_id(self
, initial_experience_id
)],
254 'display_id': display_id
,
255 'duration': duration
,
256 'title': episode
['episodeTitle'],
257 'description': episode
.get('episodeSummary'),
258 'episode': episode
.get('episodeTitle'),
259 'episode_number': int_or_none(episode
.get('episodeId')),
260 'episode_id': episode_id
,
261 'season': season
.get('seasonTitle'),
262 'season_number': int_or_none(season
.get('seasonId')),
263 'season_id': str_or_none(season
.get('seasonPk')),
264 'series': show
.get('showTitle'),
266 'thumbnails': thumbnails
,
267 'subtitles': subtitles
,
268 '_format_sort_fields': ('lang', 'source'),
271 def _get_subtitles(self
, subtitles
, experience_id
, episode
, display_id
, format_name
):
272 if isinstance(episode
, str):
273 webpage
= self
._download
_webpage
(
274 f
'https://www.funimation.com/player/{experience_id}/', display_id
,
275 fatal
=False, note
=f
'Downloading player webpage for {format_name}')
276 episode
, _
, _
= self
._get
_episode
(webpage
, episode_id
=episode
, fatal
=False)
278 for _
, version
, f
in self
._get
_experiences
(episode
):
279 for source
in f
.get('sources'):
280 for text_track
in source
.get('textTracks'):
281 if not text_track
.get('src'):
283 sub_type
= text_track
.get('type').upper()
284 sub_type
= sub_type
if sub_type
!= 'FULL' else None
286 'url': text_track
['src'],
287 'name': join_nonempty(version
, text_track
.get('label'), sub_type
, delim
=' '),
289 lang
= join_nonempty(text_track
.get('language', 'und'),
290 version
if version
!= 'Simulcast' else None,
292 if current_sub
not in subtitles
.get(lang
, []):
293 subtitles
.setdefault(lang
, []).append(current_sub
)
297 class FunimationShowIE(FunimationBaseIE
):
298 IE_NAME
= 'funimation:show'
299 _VALID_URL
= r
'(?P<url>https?://(?:www\.)?funimation(?:\.com|now\.uk)/(?P<locale>[^/]+)?/?shows/(?P<id>[^/?#&]+))/?(?:[?#]|$)'
302 'url': 'https://www.funimation.com/en/shows/sk8-the-infinity',
305 'title': 'SK8 the Infinity',
307 'playlist_count': 13,
309 'skip_download': True,
313 'url': 'https://www.funimation.com/shows/ouran-high-school-host-club/',
316 'title': 'Ouran High School Host Club',
318 'playlist_count': 26,
320 'skip_download': True,
324 def _real_initialize(self
):
326 FunimationBaseIE
._REGION
= self
._get
_region
()
328 def _real_extract(self
, url
):
329 base_url
, locale
, display_id
= self
._match
_valid
_url
(url
).groups()
331 show_info
= self
._download
_json
(
332 'https://title-api.prd.funimationsvc.com/v2/shows/{}?region={}&deviceType=web&locale={}'.format(
333 display_id
, self
._REGION
, locale
or 'en'), display_id
)
334 items_info
= self
._download
_json
(
335 'https://prod-api-funimationnow.dadcdigital.com/api/funimation/episodes/?limit=99999&title_id={}'.format(
336 show_info
.get('id')), display_id
)
338 vod_items
= traverse_obj(items_info
, ('items', ..., lambda k
, _
: re
.match(r
'(?i)mostRecent[AS]vod', k
), 'item'))
342 'id': str_or_none(show_info
['id']),
343 'title': show_info
['name'],
344 'entries': orderedSet(
346 '{}/{}'.format(base_url
, vod_item
.get('episodeSlug')), FunimationPageIE
.ie_key(),
347 vod_item
.get('episodeId'), vod_item
.get('episodeName'))
348 for vod_item
in sorted(vod_items
, key
=lambda x
: x
.get('episodeOrder', -1))),