1# coding: utf-8
2from __future__ import unicode_literals
3
4import itertools
5import json
6
7from .naver import NaverBaseIE
8from ..compat import (
9    compat_HTTPError,
10    compat_str,
11)
12from ..utils import (
13    ExtractorError,
14    int_or_none,
15    merge_dicts,
16    str_or_none,
17    strip_or_none,
18    try_get,
19    urlencode_postdata,
20)
21
22
23class VLiveBaseIE(NaverBaseIE):
24    _APP_ID = '8c6cc7b45d2568fb668be6e05b6e5a3b'
25
26
27class VLiveIE(VLiveBaseIE):
28    IE_NAME = 'vlive'
29    _VALID_URL = r'https?://(?:(?:www|m)\.)?vlive\.tv/(?:video|embed)/(?P<id>[0-9]+)'
30    _NETRC_MACHINE = 'vlive'
31    _TESTS = [{
32        'url': 'http://www.vlive.tv/video/1326',
33        'md5': 'cc7314812855ce56de70a06a27314983',
34        'info_dict': {
35            'id': '1326',
36            'ext': 'mp4',
37            'title': "Girl's Day's Broadcast",
38            'creator': "Girl's Day",
39            'view_count': int,
40            'uploader_id': 'muploader_a',
41        },
42    }, {
43        'url': 'http://www.vlive.tv/video/16937',
44        'info_dict': {
45            'id': '16937',
46            'ext': 'mp4',
47            'title': '첸백시 걍방',
48            'creator': 'EXO',
49            'view_count': int,
50            'subtitles': 'mincount:12',
51            'uploader_id': 'muploader_j',
52        },
53        'params': {
54            'skip_download': True,
55        },
56    }, {
57        'url': 'https://www.vlive.tv/video/129100',
58        'md5': 'ca2569453b79d66e5b919e5d308bff6b',
59        'info_dict': {
60            'id': '129100',
61            'ext': 'mp4',
62            'title': '[V LIVE] [BTS+] Run BTS! 2019 - EP.71 :: Behind the scene',
63            'creator': 'BTS+',
64            'view_count': int,
65            'subtitles': 'mincount:10',
66        },
67        'skip': 'This video is only available for CH+ subscribers',
68    }, {
69        'url': 'https://www.vlive.tv/embed/1326',
70        'only_matching': True,
71    }, {
72        # works only with gcc=KR
73        'url': 'https://www.vlive.tv/video/225019',
74        'only_matching': True,
75    }]
76
77    def _real_initialize(self):
78        self._login()
79
80    def _login(self):
81        email, password = self._get_login_info()
82        if None in (email, password):
83            return
84
85        def is_logged_in():
86            login_info = self._download_json(
87                'https://www.vlive.tv/auth/loginInfo', None,
88                note='Downloading login info',
89                headers={'Referer': 'https://www.vlive.tv/home'})
90            return try_get(
91                login_info, lambda x: x['message']['login'], bool) or False
92
93        LOGIN_URL = 'https://www.vlive.tv/auth/email/login'
94        self._request_webpage(
95            LOGIN_URL, None, note='Downloading login cookies')
96
97        self._download_webpage(
98            LOGIN_URL, None, note='Logging in',
99            data=urlencode_postdata({'email': email, 'pwd': password}),
100            headers={
101                'Referer': LOGIN_URL,
102                'Content-Type': 'application/x-www-form-urlencoded'
103            })
104
105        if not is_logged_in():
106            raise ExtractorError('Unable to log in', expected=True)
107
108    def _call_api(self, path_template, video_id, fields=None):
109        query = {'appId': self._APP_ID, 'gcc': 'KR', 'platformType': 'PC'}
110        if fields:
111            query['fields'] = fields
112        try:
113            return self._download_json(
114                'https://www.vlive.tv/globalv-web/vam-web/' + path_template % video_id, video_id,
115                'Downloading %s JSON metadata' % path_template.split('/')[-1].split('-')[0],
116                headers={'Referer': 'https://www.vlive.tv/'}, query=query)
117        except ExtractorError as e:
118            if isinstance(e.cause, compat_HTTPError) and e.cause.code == 403:
119                self.raise_login_required(json.loads(e.cause.read().decode('utf-8'))['message'])
120            raise
121
122    def _real_extract(self, url):
123        video_id = self._match_id(url)
124
125        post = self._call_api(
126            'post/v1.0/officialVideoPost-%s', video_id,
127            'author{nickname},channel{channelCode,channelName},officialVideo{commentCount,exposeStatus,likeCount,playCount,playTime,status,title,type,vodId}')
128
129        video = post['officialVideo']
130
131        def get_common_fields():
132            channel = post.get('channel') or {}
133            return {
134                'title': video.get('title'),
135                'creator': post.get('author', {}).get('nickname'),
136                'channel': channel.get('channelName'),
137                'channel_id': channel.get('channelCode'),
138                'duration': int_or_none(video.get('playTime')),
139                'view_count': int_or_none(video.get('playCount')),
140                'like_count': int_or_none(video.get('likeCount')),
141                'comment_count': int_or_none(video.get('commentCount')),
142            }
143
144        video_type = video.get('type')
145        if video_type == 'VOD':
146            inkey = self._call_api('video/v1.0/vod/%s/inkey', video_id)['inkey']
147            vod_id = video['vodId']
148            return merge_dicts(
149                get_common_fields(),
150                self._extract_video_info(video_id, vod_id, inkey))
151        elif video_type == 'LIVE':
152            status = video.get('status')
153            if status == 'ON_AIR':
154                stream_url = self._call_api(
155                    'old/v3/live/%s/playInfo',
156                    video_id)['result']['adaptiveStreamUrl']
157                formats = self._extract_m3u8_formats(stream_url, video_id, 'mp4')
158                self._sort_formats(formats)
159                info = get_common_fields()
160                info.update({
161                    'title': self._live_title(video['title']),
162                    'id': video_id,
163                    'formats': formats,
164                    'is_live': True,
165                })
166                return info
167            elif status == 'ENDED':
168                raise ExtractorError(
169                    'Uploading for replay. Please wait...', expected=True)
170            elif status == 'RESERVED':
171                raise ExtractorError('Coming soon!', expected=True)
172            elif video.get('exposeStatus') == 'CANCEL':
173                raise ExtractorError(
174                    'We are sorry, but the live broadcast has been canceled.',
175                    expected=True)
176            else:
177                raise ExtractorError('Unknown status ' + status)
178
179
180class VLivePostIE(VLiveIE):
181    IE_NAME = 'vlive:post'
182    _VALID_URL = r'https?://(?:(?:www|m)\.)?vlive\.tv/post/(?P<id>\d-\d+)'
183    _TESTS = [{
184        # uploadType = SOS
185        'url': 'https://www.vlive.tv/post/1-20088044',
186        'info_dict': {
187            'id': '1-20088044',
188            'title': 'Hola estrellitas la tierra les dice hola (si era así no?) Ha...',
189            'description': 'md5:fab8a1e50e6e51608907f46c7fa4b407',
190        },
191        'playlist_count': 3,
192    }, {
193        # uploadType = V
194        'url': 'https://www.vlive.tv/post/1-20087926',
195        'info_dict': {
196            'id': '1-20087926',
197            'title': 'James Corden: And so, the baby becamos the Papa��������',
198        },
199        'playlist_count': 1,
200    }]
201    _FVIDEO_TMPL = 'fvideo/v1.0/fvideo-%%s/%s'
202    _SOS_TMPL = _FVIDEO_TMPL % 'sosPlayInfo'
203    _INKEY_TMPL = _FVIDEO_TMPL % 'inKey'
204
205    def _real_extract(self, url):
206        post_id = self._match_id(url)
207
208        post = self._call_api(
209            'post/v1.0/post-%s', post_id,
210            'attachments{video},officialVideo{videoSeq},plainBody,title')
211
212        video_seq = str_or_none(try_get(
213            post, lambda x: x['officialVideo']['videoSeq']))
214        if video_seq:
215            return self.url_result(
216                'http://www.vlive.tv/video/' + video_seq,
217                VLiveIE.ie_key(), video_seq)
218
219        title = post['title']
220        entries = []
221        for idx, video in enumerate(post['attachments']['video'].values()):
222            video_id = video.get('videoId')
223            if not video_id:
224                continue
225            upload_type = video.get('uploadType')
226            upload_info = video.get('uploadInfo') or {}
227            entry = None
228            if upload_type == 'SOS':
229                download = self._call_api(
230                    self._SOS_TMPL, video_id)['videoUrl']['download']
231                formats = []
232                for f_id, f_url in download.items():
233                    formats.append({
234                        'format_id': f_id,
235                        'url': f_url,
236                        'height': int_or_none(f_id[:-1]),
237                    })
238                self._sort_formats(formats)
239                entry = {
240                    'formats': formats,
241                    'id': video_id,
242                    'thumbnail': upload_info.get('imageUrl'),
243                }
244            elif upload_type == 'V':
245                vod_id = upload_info.get('videoId')
246                if not vod_id:
247                    continue
248                inkey = self._call_api(self._INKEY_TMPL, video_id)['inKey']
249                entry = self._extract_video_info(video_id, vod_id, inkey)
250            if entry:
251                entry['title'] = '%s_part%s' % (title, idx)
252                entries.append(entry)
253        return self.playlist_result(
254            entries, post_id, title, strip_or_none(post.get('plainBody')))
255
256
257class VLiveChannelIE(VLiveBaseIE):
258    IE_NAME = 'vlive:channel'
259    _VALID_URL = r'https?://(?:channels\.vlive\.tv|(?:(?:www|m)\.)?vlive\.tv/channel)/(?P<id>[0-9A-Z]+)'
260    _TESTS = [{
261        'url': 'http://channels.vlive.tv/FCD4B',
262        'info_dict': {
263            'id': 'FCD4B',
264            'title': 'MAMAMOO',
265        },
266        'playlist_mincount': 110
267    }, {
268        'url': 'https://www.vlive.tv/channel/FCD4B',
269        'only_matching': True,
270    }]
271
272    def _call_api(self, path, channel_key_suffix, channel_value, note, query):
273        q = {
274            'app_id': self._APP_ID,
275            'channel' + channel_key_suffix: channel_value,
276        }
277        q.update(query)
278        return self._download_json(
279            'http://api.vfan.vlive.tv/vproxy/channelplus/' + path,
280            channel_value, note='Downloading ' + note, query=q)['result']
281
282    def _real_extract(self, url):
283        channel_code = self._match_id(url)
284
285        channel_seq = self._call_api(
286            'decodeChannelCode', 'Code', channel_code,
287            'decode channel code', {})['channelSeq']
288
289        channel_name = None
290        entries = []
291
292        for page_num in itertools.count(1):
293            video_list = self._call_api(
294                'getChannelVideoList', 'Seq', channel_seq,
295                'channel list page #%d' % page_num, {
296                    # Large values of maxNumOfRows (~300 or above) may cause
297                    # empty responses (see [1]), e.g. this happens for [2] that
298                    # has more than 300 videos.
299                    # 1. https://github.com/ytdl-org/youtube-dl/issues/13830
300                    # 2. http://channels.vlive.tv/EDBF.
301                    'maxNumOfRows': 100,
302                    'pageNo': page_num
303                }
304            )
305
306            if not channel_name:
307                channel_name = try_get(
308                    video_list,
309                    lambda x: x['channelInfo']['channelName'],
310                    compat_str)
311
312            videos = try_get(
313                video_list, lambda x: x['videoList'], list)
314            if not videos:
315                break
316
317            for video in videos:
318                video_id = video.get('videoSeq')
319                if not video_id:
320                    continue
321                video_id = compat_str(video_id)
322                entries.append(
323                    self.url_result(
324                        'http://www.vlive.tv/video/%s' % video_id,
325                        ie=VLiveIE.ie_key(), video_id=video_id))
326
327        return self.playlist_result(
328            entries, channel_code, channel_name)
329