]> jfr.im git - yt-dlp.git/blame - yt_dlp/extractor/funimation.py
Update to ytdl-commit-a726009
[yt-dlp.git] / yt_dlp / extractor / funimation.py
CommitLineData
ab4bdc91
MS
1# coding: utf-8
2from __future__ import unicode_literals
f542a3d2 3
929ba399
RA
4import random
5import string
6
ab4bdc91 7from .common import InfoExtractor
804181dd 8from ..compat import compat_HTTPError
ab4bdc91 9from ..utils import (
f542a3d2 10 determine_ext,
f377f44d 11 int_or_none,
91399b2f 12 js_to_json,
ab4bdc91
MS
13 ExtractorError,
14 urlencode_postdata
15)
ab4bdc91 16
b4c299ba 17
ab4bdc91 18class FunimationIE(InfoExtractor):
41d1cca3 19 _VALID_URL = r'https?://(?:www\.)?funimation(?:\.com|now\.uk)/(?:[^/]+/)?shows/[^/]+/(?P<id>[^/?#&]+)'
ab4bdc91 20
0014ffa8 21 _NETRC_MACHINE = 'funimation'
8fa17117 22 _TOKEN = None
0014ffa8 23
b59623ef 24 _TESTS = [{
91399b2f 25 'url': 'https://www.funimation.com/shows/hacksign/role-play/',
ab4bdc91 26 'info_dict': {
91399b2f 27 'id': '91144',
b59623ef
S
28 'display_id': 'role-play',
29 'ext': 'mp4',
91399b2f 30 'title': '.hack//SIGN - Role Play',
b59623ef 31 'description': 'md5:b602bdc15eef4c9bbb201bb6e6a4a2dd',
ec85ded8 32 'thumbnail': r're:https?://.*\.jpg',
b59623ef 33 },
91399b2f
RA
34 'params': {
35 # m3u8 download
36 'skip_download': True,
37 },
b091529a 38 }, {
91399b2f 39 'url': 'https://www.funimation.com/shows/attack-on-titan-junior-high/broadcast-dub-preview/',
0b1bb1ac 40 'info_dict': {
804181dd 41 'id': '210051',
0b1bb1ac
S
42 'display_id': 'broadcast-dub-preview',
43 'ext': 'mp4',
44 'title': 'Attack on Titan: Junior High - Broadcast Dub Preview',
ec85ded8 45 'thumbnail': r're:https?://.*\.(?:jpg|png)',
0b1bb1ac 46 },
804181dd
RA
47 'params': {
48 # m3u8 download
49 'skip_download': True,
50 },
91399b2f
RA
51 }, {
52 'url': 'https://www.funimationnow.uk/shows/puzzle-dragons-x/drop-impact/simulcast/',
53 'only_matching': True,
41d1cca3 54 }, {
55 # with lang code
56 'url': 'https://www.funimation.com/en/shows/hacksign/role-play/',
57 'only_matching': True,
b59623ef 58 }]
f542a3d2 59
ab4bdc91 60 def _login(self):
68217024 61 username, password = self._get_login_info()
ab4bdc91
MS
62 if username is None:
63 return
8fa17117
RA
64 try:
65 data = self._download_json(
66 'https://prod-api-funimationnow.dadcdigital.com/api/auth/login/',
e4d95865 67 None, 'Logging in', data=urlencode_postdata({
8fa17117
RA
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
ab4bdc91
MS
77
78 def _real_initialize(self):
79 self._login()
80
81 def _real_extract(self, url):
f542a3d2 82 display_id = self._match_id(url)
91399b2f 83 webpage = self._download_webpage(url, display_id)
f542a3d2 84
91399b2f
RA
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+)';",
929ba399 97 r'<iframe[^>]+src="/player/(\d+)',
91399b2f
RA
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)
f542a3d2 112
91399b2f 113 try:
8fa17117
RA
114 headers = {}
115 if self._TOKEN:
116 headers['Authorization'] = 'Token %s' % self._TOKEN
91399b2f 117 sources = self._download_json(
929ba399
RA
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']
91399b2f
RA
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
f542a3d2 128
91399b2f
RA
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 })
b59623ef
S
144 self._sort_formats(formats)
145
ab4bdc91
MS
146 return {
147 'id': video_id,
f542a3d2
S
148 'display_id': display_id,
149 'title': title,
150 'description': description,
91399b2f
RA
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'),
ab4bdc91 157 'formats': formats,
ab4bdc91 158 }