afreecatv.py (14564B)
1 # coding: utf-8 2 from __future__ import unicode_literals 3 4 import re 5 6 from .common import InfoExtractor 7 from ..compat import compat_xpath 8 from ..utils import ( 9 determine_ext, 10 ExtractorError, 11 int_or_none, 12 url_or_none, 13 urlencode_postdata, 14 xpath_text, 15 ) 16 17 18 class AfreecaTVIE(InfoExtractor): 19 IE_NAME = 'afreecatv' 20 IE_DESC = 'afreecatv.com' 21 _VALID_URL = r'''(?x) 22 https?:// 23 (?: 24 (?:(?:live|afbbs|www)\.)?afreeca(?:tv)?\.com(?::\d+)? 25 (?: 26 /app/(?:index|read_ucc_bbs)\.cgi| 27 /player/[Pp]layer\.(?:swf|html) 28 )\?.*?\bnTitleNo=| 29 vod\.afreecatv\.com/PLAYER/STATION/ 30 ) 31 (?P<id>\d+) 32 ''' 33 _NETRC_MACHINE = 'afreecatv' 34 _TESTS = [{ 35 'url': 'http://live.afreecatv.com:8079/app/index.cgi?szType=read_ucc_bbs&szBjId=dailyapril&nStationNo=16711924&nBbsNo=18605867&nTitleNo=36164052&szSkin=', 36 'md5': 'f72c89fe7ecc14c1b5ce506c4996046e', 37 'info_dict': { 38 'id': '36164052', 39 'ext': 'mp4', 40 'title': '데일리 에이프릴 요정들의 시상식!', 41 'thumbnail': 're:^https?://(?:video|st)img.afreecatv.com/.*$', 42 'uploader': 'dailyapril', 43 'uploader_id': 'dailyapril', 44 'upload_date': '20160503', 45 }, 46 'skip': 'Video is gone', 47 }, { 48 'url': 'http://afbbs.afreecatv.com:8080/app/read_ucc_bbs.cgi?nStationNo=16711924&nTitleNo=36153164&szBjId=dailyapril&nBbsNo=18605867', 49 'info_dict': { 50 'id': '36153164', 51 'title': "BJ유트루와 함께하는 '팅커벨 메이크업!'", 52 'thumbnail': 're:^https?://(?:video|st)img.afreecatv.com/.*$', 53 'uploader': 'dailyapril', 54 'uploader_id': 'dailyapril', 55 }, 56 'playlist_count': 2, 57 'playlist': [{ 58 'md5': 'd8b7c174568da61d774ef0203159bf97', 59 'info_dict': { 60 'id': '36153164_1', 61 'ext': 'mp4', 62 'title': "BJ유트루와 함께하는 '팅커벨 메이크업!'", 63 'upload_date': '20160502', 64 }, 65 }, { 66 'md5': '58f2ce7f6044e34439ab2d50612ab02b', 67 'info_dict': { 68 'id': '36153164_2', 69 'ext': 'mp4', 70 'title': "BJ유트루와 함께하는 '팅커벨 메이크업!'", 71 'upload_date': '20160502', 72 }, 73 }], 74 'skip': 'Video is gone', 75 }, { 76 'url': 'http://vod.afreecatv.com/PLAYER/STATION/18650793', 77 'info_dict': { 78 'id': '18650793', 79 'ext': 'mp4', 80 'title': '오늘은 다르다! 쏘님의 우월한 위아래~ 댄스리액션!', 81 'thumbnail': r're:^https?://.*\.jpg$', 82 'uploader': '윈아디', 83 'uploader_id': 'badkids', 84 'duration': 107, 85 }, 86 'params': { 87 'skip_download': True, 88 }, 89 }, { 90 'url': 'http://vod.afreecatv.com/PLAYER/STATION/10481652', 91 'info_dict': { 92 'id': '10481652', 93 'title': "BJ유트루와 함께하는 '팅커벨 메이크업!'", 94 'thumbnail': 're:^https?://(?:video|st)img.afreecatv.com/.*$', 95 'uploader': 'dailyapril', 96 'uploader_id': 'dailyapril', 97 'duration': 6492, 98 }, 99 'playlist_count': 2, 100 'playlist': [{ 101 'md5': 'd8b7c174568da61d774ef0203159bf97', 102 'info_dict': { 103 'id': '20160502_c4c62b9d_174361386_1', 104 'ext': 'mp4', 105 'title': "BJ유트루와 함께하는 '팅커벨 메이크업!' (part 1)", 106 'thumbnail': 're:^https?://(?:video|st)img.afreecatv.com/.*$', 107 'uploader': 'dailyapril', 108 'uploader_id': 'dailyapril', 109 'upload_date': '20160502', 110 'duration': 3601, 111 }, 112 }, { 113 'md5': '58f2ce7f6044e34439ab2d50612ab02b', 114 'info_dict': { 115 'id': '20160502_39e739bb_174361386_2', 116 'ext': 'mp4', 117 'title': "BJ유트루와 함께하는 '팅커벨 메이크업!' (part 2)", 118 'thumbnail': 're:^https?://(?:video|st)img.afreecatv.com/.*$', 119 'uploader': 'dailyapril', 120 'uploader_id': 'dailyapril', 121 'upload_date': '20160502', 122 'duration': 2891, 123 }, 124 }], 125 'params': { 126 'skip_download': True, 127 }, 128 }, { 129 # non standard key 130 'url': 'http://vod.afreecatv.com/PLAYER/STATION/20515605', 131 'info_dict': { 132 'id': '20170411_BE689A0E_190960999_1_2_h', 133 'ext': 'mp4', 134 'title': '혼자사는여자집', 135 'thumbnail': 're:^https?://(?:video|st)img.afreecatv.com/.*$', 136 'uploader': '♥이슬이', 137 'uploader_id': 'dasl8121', 138 'upload_date': '20170411', 139 'duration': 213, 140 }, 141 'params': { 142 'skip_download': True, 143 }, 144 }, { 145 # PARTIAL_ADULT 146 'url': 'http://vod.afreecatv.com/PLAYER/STATION/32028439', 147 'info_dict': { 148 'id': '20180327_27901457_202289533_1', 149 'ext': 'mp4', 150 'title': '[생]빨개요♥ (part 1)', 151 'thumbnail': 're:^https?://(?:video|st)img.afreecatv.com/.*$', 152 'uploader': '[SA]서아', 153 'uploader_id': 'bjdyrksu', 154 'upload_date': '20180327', 155 'duration': 3601, 156 }, 157 'params': { 158 'skip_download': True, 159 }, 160 'expected_warnings': ['adult content'], 161 }, { 162 'url': 'http://www.afreecatv.com/player/Player.swf?szType=szBjId=djleegoon&nStationNo=11273158&nBbsNo=13161095&nTitleNo=36327652', 163 'only_matching': True, 164 }, { 165 'url': 'http://vod.afreecatv.com/PLAYER/STATION/15055030', 166 'only_matching': True, 167 }] 168 169 @staticmethod 170 def parse_video_key(key): 171 video_key = {} 172 m = re.match(r'^(?P<upload_date>\d{8})_\w+_(?P<part>\d+)$', key) 173 if m: 174 video_key['upload_date'] = m.group('upload_date') 175 video_key['part'] = int(m.group('part')) 176 return video_key 177 178 def _real_initialize(self): 179 self._login() 180 181 def _login(self): 182 username, password = self._get_login_info() 183 if username is None: 184 return 185 186 login_form = { 187 'szWork': 'login', 188 'szType': 'json', 189 'szUid': username, 190 'szPassword': password, 191 'isSaveId': 'false', 192 'szScriptVar': 'oLoginRet', 193 'szAction': '', 194 } 195 196 response = self._download_json( 197 'https://login.afreecatv.com/app/LoginAction.php', None, 198 'Logging in', data=urlencode_postdata(login_form)) 199 200 _ERRORS = { 201 -4: 'Your account has been suspended due to a violation of our terms and policies.', 202 -5: 'https://member.afreecatv.com/app/user_delete_progress.php', 203 -6: 'https://login.afreecatv.com/membership/changeMember.php', 204 -8: "Hello! AfreecaTV here.\nThe username you have entered belongs to \n an account that requires a legal guardian's consent. \nIf you wish to use our services without restriction, \nplease make sure to go through the necessary verification process.", 205 -9: 'https://member.afreecatv.com/app/pop_login_block.php', 206 -11: 'https://login.afreecatv.com/afreeca/second_login.php', 207 -12: 'https://member.afreecatv.com/app/user_security.php', 208 0: 'The username does not exist or you have entered the wrong password.', 209 -1: 'The username does not exist or you have entered the wrong password.', 210 -3: 'You have entered your username/password incorrectly.', 211 -7: 'You cannot use your Global AfreecaTV account to access Korean AfreecaTV.', 212 -10: 'Sorry for the inconvenience. \nYour account has been blocked due to an unauthorized access. \nPlease contact our Help Center for assistance.', 213 -32008: 'You have failed to log in. Please contact our Help Center.', 214 } 215 216 result = int_or_none(response.get('RESULT')) 217 if result != 1: 218 error = _ERRORS.get(result, 'You have failed to log in.') 219 raise ExtractorError( 220 'Unable to login: %s said: %s' % (self.IE_NAME, error), 221 expected=True) 222 223 def _real_extract(self, url): 224 video_id = self._match_id(url) 225 226 webpage = self._download_webpage(url, video_id) 227 228 if re.search(r'alert\(["\']This video has been deleted', webpage): 229 raise ExtractorError( 230 'Video %s has been deleted' % video_id, expected=True) 231 232 station_id = self._search_regex( 233 r'nStationNo\s*=\s*(\d+)', webpage, 'station') 234 bbs_id = self._search_regex( 235 r'nBbsNo\s*=\s*(\d+)', webpage, 'bbs') 236 video_id = self._search_regex( 237 r'nTitleNo\s*=\s*(\d+)', webpage, 'title', default=video_id) 238 239 partial_view = False 240 for _ in range(2): 241 query = { 242 'nTitleNo': video_id, 243 'nStationNo': station_id, 244 'nBbsNo': bbs_id, 245 } 246 if partial_view: 247 query['partialView'] = 'SKIP_ADULT' 248 video_xml = self._download_xml( 249 'http://afbbs.afreecatv.com:8080/api/video/get_video_info.php', 250 video_id, 'Downloading video info XML%s' 251 % (' (skipping adult)' if partial_view else ''), 252 video_id, headers={ 253 'Referer': url, 254 }, query=query) 255 256 flag = xpath_text(video_xml, './track/flag', 'flag', default=None) 257 if flag and flag == 'SUCCEED': 258 break 259 if flag == 'PARTIAL_ADULT': 260 self._downloader.report_warning( 261 'In accordance with local laws and regulations, underage users are restricted from watching adult content. ' 262 'Only content suitable for all ages will be downloaded. ' 263 'Provide account credentials if you wish to download restricted content.') 264 partial_view = True 265 continue 266 elif flag == 'ADULT': 267 error = 'Only users older than 19 are able to watch this video. Provide account credentials to download this content.' 268 else: 269 error = flag 270 raise ExtractorError( 271 '%s said: %s' % (self.IE_NAME, error), expected=True) 272 else: 273 raise ExtractorError('Unable to download video info') 274 275 video_element = video_xml.findall(compat_xpath('./track/video'))[-1] 276 if video_element is None or video_element.text is None: 277 raise ExtractorError( 278 'Video %s does not exist' % video_id, expected=True) 279 280 video_url = video_element.text.strip() 281 282 title = xpath_text(video_xml, './track/title', 'title', fatal=True) 283 284 uploader = xpath_text(video_xml, './track/nickname', 'uploader') 285 uploader_id = xpath_text(video_xml, './track/bj_id', 'uploader id') 286 duration = int_or_none(xpath_text( 287 video_xml, './track/duration', 'duration')) 288 thumbnail = xpath_text(video_xml, './track/titleImage', 'thumbnail') 289 290 common_entry = { 291 'uploader': uploader, 292 'uploader_id': uploader_id, 293 'thumbnail': thumbnail, 294 } 295 296 info = common_entry.copy() 297 info.update({ 298 'id': video_id, 299 'title': title, 300 'duration': duration, 301 }) 302 303 if not video_url: 304 entries = [] 305 file_elements = video_element.findall(compat_xpath('./file')) 306 one = len(file_elements) == 1 307 for file_num, file_element in enumerate(file_elements, start=1): 308 file_url = url_or_none(file_element.text) 309 if not file_url: 310 continue 311 key = file_element.get('key', '') 312 upload_date = self._search_regex( 313 r'^(\d{8})_', key, 'upload date', default=None) 314 file_duration = int_or_none(file_element.get('duration')) 315 format_id = key if key else '%s_%s' % (video_id, file_num) 316 if determine_ext(file_url) == 'm3u8': 317 formats = self._extract_m3u8_formats( 318 file_url, video_id, 'mp4', entry_protocol='m3u8_native', 319 m3u8_id='hls', 320 note='Downloading part %d m3u8 information' % file_num) 321 else: 322 formats = [{ 323 'url': file_url, 324 'format_id': 'http', 325 }] 326 if not formats: 327 continue 328 self._sort_formats(formats) 329 file_info = common_entry.copy() 330 file_info.update({ 331 'id': format_id, 332 'title': title if one else '%s (part %d)' % (title, file_num), 333 'upload_date': upload_date, 334 'duration': file_duration, 335 'formats': formats, 336 }) 337 entries.append(file_info) 338 entries_info = info.copy() 339 entries_info.update({ 340 '_type': 'multi_video', 341 'entries': entries, 342 }) 343 return entries_info 344 345 info = { 346 'id': video_id, 347 'title': title, 348 'uploader': uploader, 349 'uploader_id': uploader_id, 350 'duration': duration, 351 'thumbnail': thumbnail, 352 } 353 354 if determine_ext(video_url) == 'm3u8': 355 info['formats'] = self._extract_m3u8_formats( 356 video_url, video_id, 'mp4', entry_protocol='m3u8_native', 357 m3u8_id='hls') 358 else: 359 app, playpath = video_url.split('mp4:') 360 info.update({ 361 'url': app, 362 'ext': 'flv', 363 'play_path': 'mp4:' + playpath, 364 'rtmp_live': True, # downloading won't end without this 365 }) 366 367 return info