2022-04-07 11:03:13 +00:00
import functools
2016-05-06 03:07:29 +00:00
2016-05-04 18:11:04 +00:00
from . common import InfoExtractor
from . . utils import (
2022-04-07 11:03:13 +00:00
ExtractorError ,
OnDemandPagedList ,
2024-04-04 16:48:05 +00:00
UserNotLive ,
2017-04-10 19:05:53 +00:00
determine_ext ,
2024-04-04 16:48:05 +00:00
filter_dict ,
2016-05-04 18:11:04 +00:00
int_or_none ,
2024-04-13 16:40:53 +00:00
orderedSet ,
2022-01-07 12:12:29 +00:00
unified_timestamp ,
2018-07-21 12:08:28 +00:00
url_or_none ,
2018-04-01 15:47:39 +00:00
urlencode_postdata ,
2024-04-13 16:40:53 +00:00
urljoin ,
2016-05-04 18:11:04 +00:00
)
2024-04-04 16:48:05 +00:00
from . . utils . traversal import traverse_obj
2016-05-04 18:11:04 +00:00
2024-04-04 16:48:05 +00:00
class AfreecaTVBaseIE ( InfoExtractor ) :
_NETRC_MACHINE = ' afreecatv '
def _perform_login ( self , username , password ) :
login_form = {
' szWork ' : ' login ' ,
' szType ' : ' json ' ,
' szUid ' : username ,
' szPassword ' : password ,
' isSaveId ' : ' false ' ,
' szScriptVar ' : ' oLoginRet ' ,
' szAction ' : ' ' ,
}
response = self . _download_json (
' https://login.afreecatv.com/app/LoginAction.php ' , None ,
' Logging in ' , data = urlencode_postdata ( login_form ) )
_ERRORS = {
- 4 : ' Your account has been suspended due to a violation of our terms and policies. ' ,
- 5 : ' https://member.afreecatv.com/app/user_delete_progress.php ' ,
- 6 : ' https://login.afreecatv.com/membership/changeMember.php ' ,
- 8 : " Hello! AfreecaTV here. \n The username you have entered belongs to \n an account that requires a legal guardian ' s consent. \n If you wish to use our services without restriction, \n please make sure to go through the necessary verification process. " ,
- 9 : ' https://member.afreecatv.com/app/pop_login_block.php ' ,
- 11 : ' https://login.afreecatv.com/afreeca/second_login.php ' ,
- 12 : ' https://member.afreecatv.com/app/user_security.php ' ,
0 : ' The username does not exist or you have entered the wrong password. ' ,
- 1 : ' The username does not exist or you have entered the wrong password. ' ,
- 3 : ' You have entered your username/password incorrectly. ' ,
- 7 : ' You cannot use your Global AfreecaTV account to access Korean AfreecaTV. ' ,
- 10 : ' Sorry for the inconvenience. \n Your account has been blocked due to an unauthorized access. \n Please contact our Help Center for assistance. ' ,
- 32008 : ' You have failed to log in. Please contact our Help Center. ' ,
}
result = int_or_none ( response . get ( ' RESULT ' ) )
if result != 1 :
error = _ERRORS . get ( result , ' You have failed to log in. ' )
raise ExtractorError (
' Unable to login: %s said: %s ' % ( self . IE_NAME , error ) ,
expected = True )
class AfreecaTVIE ( AfreecaTVBaseIE ) :
2017-01-25 06:38:17 +00:00
IE_NAME = ' afreecatv '
2016-05-04 18:11:04 +00:00
IE_DESC = ' afreecatv.com '
2016-11-12 23:02:26 +00:00
_VALID_URL = r ''' (?x)
https ? : / /
( ? :
( ? : ( ? : live | afbbs | www ) \. ) ? afreeca ( ? : tv ) ? \. com ( ? : : \d + ) ?
( ? :
/ app / ( ? : index | read_ucc_bbs ) \. cgi |
/ player / [ Pp ] layer \. ( ? : swf | html )
) \? . * ? \bnTitleNo = |
2022-03-18 09:53:07 +00:00
vod \. afreecatv \. com / ( PLAYER / STATION | player ) /
2016-11-12 23:02:26 +00:00
)
( ? P < id > \d + )
'''
2016-05-06 03:08:43 +00:00
_TESTS = [ {
2016-05-04 18:11:04 +00:00
' url ' : ' http://live.afreecatv.com:8079/app/index.cgi?szType=read_ucc_bbs&szBjId=dailyapril&nStationNo=16711924&nBbsNo=18605867&nTitleNo=36164052&szSkin= ' ,
' md5 ' : ' f72c89fe7ecc14c1b5ce506c4996046e ' ,
' info_dict ' : {
' id ' : ' 36164052 ' ,
' ext ' : ' mp4 ' ,
' title ' : ' 데일리 에이프릴 요정들의 시상식! ' ,
2016-05-08 01:02:19 +00:00
' thumbnail ' : ' re:^https?://(?:video|st)img.afreecatv.com/.*$ ' ,
2016-05-04 18:11:04 +00:00
' uploader ' : ' dailyapril ' ,
' uploader_id ' : ' dailyapril ' ,
2016-05-06 03:08:43 +00:00
' upload_date ' : ' 20160503 ' ,
2017-03-05 16:31:44 +00:00
} ,
' skip ' : ' Video is gone ' ,
2016-05-06 03:08:43 +00:00
} , {
' url ' : ' http://afbbs.afreecatv.com:8080/app/read_ucc_bbs.cgi?nStationNo=16711924&nTitleNo=36153164&szBjId=dailyapril&nBbsNo=18605867 ' ,
' info_dict ' : {
' id ' : ' 36153164 ' ,
' title ' : " BJ유트루와 함께하는 ' 팅커벨 메이크업! ' " ,
2016-05-08 01:02:19 +00:00
' thumbnail ' : ' re:^https?://(?:video|st)img.afreecatv.com/.*$ ' ,
2016-05-06 03:08:43 +00:00
' uploader ' : ' dailyapril ' ,
' uploader_id ' : ' dailyapril ' ,
} ,
' playlist_count ' : 2 ,
' playlist ' : [ {
' md5 ' : ' d8b7c174568da61d774ef0203159bf97 ' ,
' info_dict ' : {
' id ' : ' 36153164_1 ' ,
' ext ' : ' mp4 ' ,
' title ' : " BJ유트루와 함께하는 ' 팅커벨 메이크업! ' " ,
' upload_date ' : ' 20160502 ' ,
} ,
} , {
' md5 ' : ' 58f2ce7f6044e34439ab2d50612ab02b ' ,
' info_dict ' : {
' id ' : ' 36153164_2 ' ,
' ext ' : ' mp4 ' ,
' title ' : " BJ유트루와 함께하는 ' 팅커벨 메이크업! ' " ,
' upload_date ' : ' 20160502 ' ,
} ,
} ] ,
2017-03-05 16:31:44 +00:00
' skip ' : ' Video is gone ' ,
2017-04-11 19:15:37 +00:00
} , {
# non standard key
' url ' : ' http://vod.afreecatv.com/PLAYER/STATION/20515605 ' ,
' info_dict ' : {
' id ' : ' 20170411_BE689A0E_190960999_1_2_h ' ,
' ext ' : ' mp4 ' ,
' title ' : ' 혼자사는여자집 ' ,
' thumbnail ' : ' re:^https?://(?:video|st)img.afreecatv.com/.*$ ' ,
' uploader ' : ' ♥이슬이 ' ,
' uploader_id ' : ' dasl8121 ' ,
' upload_date ' : ' 20170411 ' ,
2024-04-06 17:23:16 +00:00
' timestamp ' : 1491929865 ,
2017-04-11 19:15:37 +00:00
' duration ' : 213 ,
} ,
' params ' : {
' skip_download ' : True ,
} ,
2017-10-01 20:28:25 +00:00
} , {
2023-06-14 19:01:18 +00:00
# adult content
' url ' : ' https://vod.afreecatv.com/player/97267690 ' ,
2017-10-01 20:28:25 +00:00
' info_dict ' : {
2018-04-01 17:00:45 +00:00
' id ' : ' 20180327_27901457_202289533_1 ' ,
2017-10-01 20:28:25 +00:00
' ext ' : ' mp4 ' ,
2018-04-01 17:00:45 +00:00
' title ' : ' [생]빨개요♥ (part 1) ' ,
2017-10-01 20:28:25 +00:00
' thumbnail ' : ' re:^https?://(?:video|st)img.afreecatv.com/.*$ ' ,
2018-04-01 17:00:45 +00:00
' uploader ' : ' [SA]서아 ' ,
2017-10-01 20:28:25 +00:00
' uploader_id ' : ' bjdyrksu ' ,
2018-04-01 17:00:45 +00:00
' upload_date ' : ' 20180327 ' ,
' duration ' : 3601 ,
2017-10-01 20:28:25 +00:00
} ,
' params ' : {
' skip_download ' : True ,
} ,
2023-06-14 19:01:18 +00:00
' skip ' : ' The VOD does not exist ' ,
2016-05-08 01:02:19 +00:00
} , {
' url ' : ' http://www.afreecatv.com/player/Player.swf?szType=szBjId=djleegoon&nStationNo=11273158&nBbsNo=13161095&nTitleNo=36327652 ' ,
' only_matching ' : True ,
2016-11-12 23:02:26 +00:00
} , {
2023-06-14 19:01:18 +00:00
' url ' : ' https://vod.afreecatv.com/player/96753363 ' ,
' info_dict ' : {
' id ' : ' 20230108_9FF5BEE1_244432674_1 ' ,
' ext ' : ' mp4 ' ,
' uploader_id ' : ' rlantnghks ' ,
' uploader ' : ' 페이즈으 ' ,
' duration ' : 10840 ,
2024-04-06 17:23:16 +00:00
' thumbnail ' : r ' re:https?://videoimg \ .afreecatv \ .com/.+ ' ,
2023-06-14 19:01:18 +00:00
' upload_date ' : ' 20230108 ' ,
2024-04-06 17:23:16 +00:00
' timestamp ' : 1673218805 ,
2023-06-14 19:01:18 +00:00
' title ' : ' 젠지 페이즈 ' ,
} ,
' params ' : {
' skip_download ' : True ,
} ,
2024-04-06 17:23:16 +00:00
} , {
# adult content
' url ' : ' https://vod.afreecatv.com/player/70395877 ' ,
' only_matching ' : True ,
} , {
# subscribers only
' url ' : ' https://vod.afreecatv.com/player/104647403 ' ,
' only_matching ' : True ,
} , {
# private
' url ' : ' https://vod.afreecatv.com/player/81669846 ' ,
' only_matching ' : True ,
2016-05-06 03:08:43 +00:00
} ]
2016-05-04 18:11:04 +00:00
def _real_extract ( self , url ) :
video_id = self . _match_id ( url )
2024-04-06 17:23:16 +00:00
data = self . _download_json (
' https://api.m.afreecatv.com/station/video/a/view ' , video_id ,
headers = { ' Referer ' : url } , data = urlencode_postdata ( {
2017-10-01 20:28:25 +00:00
' nTitleNo ' : video_id ,
2024-04-06 17:23:16 +00:00
' nApiLevel ' : 10 ,
} ) ) [ ' data ' ]
2017-10-01 20:28:25 +00:00
2024-04-06 17:23:16 +00:00
error_code = traverse_obj ( data , ( ' code ' , { int } ) )
if error_code == - 6221 :
raise ExtractorError ( ' The VOD does not exist ' , expected = True )
elif error_code == - 6205 :
raise ExtractorError ( ' This VOD is private ' , expected = True )
2016-05-04 18:11:04 +00:00
2024-04-06 17:23:16 +00:00
common_info = traverse_obj ( data , {
' title ' : ( ' title ' , { str } ) ,
' uploader ' : ( ' writer_nick ' , { str } ) ,
' uploader_id ' : ( ' bj_id ' , { str } ) ,
' duration ' : ( ' total_file_duration ' , { functools . partial ( int_or_none , scale = 1000 ) } ) ,
' thumbnail ' : ( ' thumb ' , { url_or_none } ) ,
2017-04-10 19:05:53 +00:00
} )
2024-04-06 17:23:16 +00:00
entries = [ ]
for file_num , file_element in enumerate (
traverse_obj ( data , ( ' files ' , lambda _ , v : url_or_none ( v [ ' file ' ] ) ) ) , start = 1 ) :
file_url = file_element [ ' file ' ]
if determine_ext ( file_url ) == ' m3u8 ' :
formats = self . _extract_m3u8_formats (
file_url , video_id , ' mp4 ' , m3u8_id = ' hls ' ,
note = f ' Downloading part { file_num } m3u8 information ' )
else :
formats = [ {
' url ' : file_url ,
' format_id ' : ' http ' ,
} ]
entries . append ( {
* * common_info ,
' id ' : file_element . get ( ' file_info_key ' ) or f ' { video_id } _ { file_num } ' ,
' title ' : f ' { common_info . get ( " title " ) or " Untitled " } (part { file_num } ) ' ,
' formats ' : formats ,
* * traverse_obj ( file_element , {
' duration ' : ( ' duration ' , { functools . partial ( int_or_none , scale = 1000 ) } ) ,
' timestamp ' : ( ' file_start ' , { unified_timestamp } ) ,
2017-04-10 19:05:53 +00:00
} )
} )
2024-04-06 17:23:16 +00:00
if traverse_obj ( data , ( ' adult_status ' , { str } ) ) == ' notLogin ' :
if not entries :
self . raise_login_required (
' Only users older than 19 are able to watch this video ' , method = ' password ' )
self . report_warning (
' In accordance with local laws and regulations, underage users are '
' restricted from watching adult content. Only content suitable for all '
f ' ages will be downloaded. { self . _login_hint ( " password " ) } ' )
2016-05-04 18:11:04 +00:00
2024-04-06 17:23:16 +00:00
if not entries and traverse_obj ( data , ( ' sub_upload_type ' , { str } ) ) :
self . raise_login_required ( ' This VOD is for subscribers only ' , method = ' password ' )
if len ( entries ) == 1 :
return {
* * entries [ 0 ] ,
' title ' : common_info . get ( ' title ' ) ,
}
common_info [ ' timestamp ' ] = traverse_obj ( entries , ( . . . , ' timestamp ' ) , get_all = False )
2017-04-10 19:05:53 +00:00
2024-04-06 17:23:16 +00:00
return self . playlist_result ( entries , video_id , multi_video = True , * * common_info )
2022-01-07 12:12:29 +00:00
2024-04-04 16:48:05 +00:00
class AfreecaTVLiveIE ( AfreecaTVBaseIE ) :
2022-01-07 12:12:29 +00:00
IE_NAME = ' afreecatv:live '
2024-04-04 16:48:05 +00:00
IE_DESC = ' afreecatv.com livestreams '
2022-01-07 12:12:29 +00:00
_VALID_URL = r ' https?://play \ .afreeca(?:tv)? \ .com/(?P<id>[^/]+)(?:/(?P<bno> \ d+))? '
_TESTS = [ {
' url ' : ' https://play.afreecatv.com/pyh3646/237852185 ' ,
' info_dict ' : {
' id ' : ' 237852185 ' ,
' ext ' : ' mp4 ' ,
' title ' : ' 【 우루과이 오늘은 무슨일이? 】 ' ,
' uploader ' : ' 박진우[JINU] ' ,
' uploader_id ' : ' pyh3646 ' ,
' timestamp ' : 1640661495 ,
' is_live ' : True ,
} ,
' skip ' : ' Livestream has ended ' ,
} , {
2024-04-04 16:48:05 +00:00
' url ' : ' https://play.afreecatv.com/pyh3646/237852185 ' ,
2022-01-07 12:12:29 +00:00
' only_matching ' : True ,
} , {
2024-04-04 16:48:05 +00:00
' url ' : ' https://play.afreecatv.com/pyh3646 ' ,
2022-01-07 12:12:29 +00:00
' only_matching ' : True ,
} ]
_LIVE_API_URL = ' https://live.afreecatv.com/afreeca/player_live_api.php '
2024-04-13 16:40:53 +00:00
_WORKING_CDNS = [
' gcp_cdn ' , # live-global-cdn-v02.afreecatv.com
' gs_cdn_pc_app ' , # pc-app.stream.afreecatv.com
' gs_cdn_mobile_web ' , # mobile-web.stream.afreecatv.com
' gs_cdn_pc_web ' , # pc-web.stream.afreecatv.com
]
_BAD_CDNS = [
' gs_cdn ' , # chromecast.afreeca.gscdn.com (cannot resolve)
' gs_cdn_chromecast ' , # chromecast.stream.afreecatv.com (HTTP Error 400)
' azure_cdn ' , # live-global-cdn-v01.afreecatv.com (cannot resolve)
' aws_cf ' , # live-global-cdn-v03.afreecatv.com (cannot resolve)
' kt_cdn ' , # kt.stream.afreecatv.com (HTTP Error 400)
]
def _extract_formats ( self , channel_info , broadcast_no , aid ) :
stream_base_url = channel_info . get ( ' RMD ' ) or ' https://livestream-manager.afreecatv.com '
# If user has not passed CDN IDs, try API-provided CDN ID followed by other working CDN IDs
default_cdn_ids = orderedSet ( [
* traverse_obj ( channel_info , ( ' CDN ' , { str } , all , lambda _ , v : v not in self . _BAD_CDNS ) ) ,
* self . _WORKING_CDNS ,
] )
cdn_ids = self . _configuration_arg ( ' cdn ' , default_cdn_ids )
for attempt , cdn_id in enumerate ( cdn_ids , start = 1 ) :
m3u8_url = traverse_obj ( self . _download_json (
urljoin ( stream_base_url , ' broad_stream_assign.html ' ) , broadcast_no ,
f ' Downloading { cdn_id } stream info ' , f ' Unable to download { cdn_id } stream info ' ,
fatal = False , query = {
' return_type ' : cdn_id ,
' broad_key ' : f ' { broadcast_no } -common-master-hls ' ,
} ) , ( ' view_url ' , { url_or_none } ) )
try :
return self . _extract_m3u8_formats (
m3u8_url , broadcast_no , ' mp4 ' , m3u8_id = ' hls ' , query = { ' aid ' : aid } ,
headers = { ' Referer ' : ' https://play.afreecatv.com/ ' } )
except ExtractorError as e :
if attempt == len ( cdn_ids ) :
raise
self . report_warning (
f ' { e . cause or e . msg } . Retrying... (attempt { attempt } of { len ( cdn_ids ) } ) ' )
2022-01-07 12:12:29 +00:00
def _real_extract ( self , url ) :
broadcaster_id , broadcast_no = self . _match_valid_url ( url ) . group ( ' id ' , ' bno ' )
2024-04-04 16:48:05 +00:00
channel_info = traverse_obj ( self . _download_json (
self . _LIVE_API_URL , broadcaster_id , data = urlencode_postdata ( { ' bid ' : broadcaster_id } ) ) ,
( ' CHANNEL ' , { dict } ) ) or { }
2022-01-07 12:12:29 +00:00
broadcaster_id = channel_info . get ( ' BJID ' ) or broadcaster_id
broadcast_no = channel_info . get ( ' BNO ' ) or broadcast_no
if not broadcast_no :
2024-04-04 16:48:05 +00:00
raise UserNotLive ( video_id = broadcaster_id )
password = self . get_param ( ' videopassword ' )
if channel_info . get ( ' BPWD ' ) == ' Y ' and password is None :
2022-02-11 14:15:59 +00:00
raise ExtractorError (
' This livestream is protected by a password, use the --video-password option ' ,
expected = True )
2022-01-07 12:12:29 +00:00
2024-04-13 16:40:53 +00:00
token_info = traverse_obj ( self . _download_json (
2024-04-04 16:48:05 +00:00
self . _LIVE_API_URL , broadcast_no , ' Downloading access token for stream ' ,
' Unable to download access token for stream ' , data = urlencode_postdata ( filter_dict ( {
2022-02-11 14:15:59 +00:00
' bno ' : broadcast_no ,
' stream_type ' : ' common ' ,
' type ' : ' aid ' ,
2024-04-04 16:48:05 +00:00
' quality ' : ' master ' ,
' pwd ' : password ,
2024-04-13 16:40:53 +00:00
} ) ) ) , ( ' CHANNEL ' , { dict } ) ) or { }
aid = token_info . get ( ' AID ' )
if not aid :
result = token_info . get ( ' RESULT ' )
if result == 0 :
raise ExtractorError ( ' This livestream has ended ' , expected = True )
elif result == - 6 :
self . raise_login_required ( ' This livestream is for subscribers only ' , method = ' password ' )
raise ExtractorError ( ' Unable to extract access token ' )
formats = self . _extract_formats ( channel_info , broadcast_no , aid )
2024-04-04 16:48:05 +00:00
station_info = traverse_obj ( self . _download_json (
2022-01-07 12:12:29 +00:00
' https://st.afreecatv.com/api/get_station_status.php ' , broadcast_no ,
2024-04-04 16:48:05 +00:00
' Downloading channel metadata ' , ' Unable to download channel metadata ' ,
query = { ' szBjId ' : broadcaster_id } , fatal = False ) , { dict } ) or { }
2022-01-07 12:12:29 +00:00
return {
' id ' : broadcast_no ,
' title ' : channel_info . get ( ' TITLE ' ) or station_info . get ( ' station_title ' ) ,
' uploader ' : channel_info . get ( ' BJNICK ' ) or station_info . get ( ' station_name ' ) ,
' uploader_id ' : broadcaster_id ,
' timestamp ' : unified_timestamp ( station_info . get ( ' broad_start ' ) ) ,
' formats ' : formats ,
' is_live ' : True ,
2024-04-04 16:48:05 +00:00
' http_headers ' : { ' Referer ' : url } ,
2022-01-07 12:12:29 +00:00
}
2022-04-07 11:03:13 +00:00
class AfreecaTVUserIE ( InfoExtractor ) :
IE_NAME = ' afreecatv:user '
_VALID_URL = r ' https?://bj \ .afreeca(?:tv)? \ .com/(?P<id>[^/]+)/vods/?(?P<slug_type>[^/]+)? '
_TESTS = [ {
' url ' : ' https://bj.afreecatv.com/ryuryu24/vods/review ' ,
' info_dict ' : {
' _type ' : ' playlist ' ,
' id ' : ' ryuryu24 ' ,
' title ' : ' ryuryu24 - review ' ,
} ,
' playlist_count ' : 218 ,
} , {
' url ' : ' https://bj.afreecatv.com/parang1995/vods/highlight ' ,
' info_dict ' : {
' _type ' : ' playlist ' ,
' id ' : ' parang1995 ' ,
' title ' : ' parang1995 - highlight ' ,
} ,
' playlist_count ' : 997 ,
} , {
' url ' : ' https://bj.afreecatv.com/ryuryu24/vods ' ,
' info_dict ' : {
' _type ' : ' playlist ' ,
' id ' : ' ryuryu24 ' ,
' title ' : ' ryuryu24 - all ' ,
} ,
' playlist_count ' : 221 ,
} , {
' url ' : ' https://bj.afreecatv.com/ryuryu24/vods/balloonclip ' ,
' info_dict ' : {
' _type ' : ' playlist ' ,
' id ' : ' ryuryu24 ' ,
' title ' : ' ryuryu24 - balloonclip ' ,
} ,
' playlist_count ' : 0 ,
} ]
_PER_PAGE = 60
def _fetch_page ( self , user_id , user_type , page ) :
page + = 1
info = self . _download_json ( f ' https://bjapi.afreecatv.com/api/ { user_id } /vods/ { user_type } ' , user_id ,
query = { ' page ' : page , ' per_page ' : self . _PER_PAGE , ' orderby ' : ' reg_date ' } ,
note = f ' Downloading { user_type } video page { page } ' )
for item in info [ ' data ' ] :
yield self . url_result (
f ' https://vod.afreecatv.com/player/ { item [ " title_no " ] } / ' , AfreecaTVIE , item [ ' title_no ' ] )
def _real_extract ( self , url ) :
user_id , user_type = self . _match_valid_url ( url ) . group ( ' id ' , ' slug_type ' )
user_type = user_type or ' all '
entries = OnDemandPagedList ( functools . partial ( self . _fetch_page , user_id , user_type ) , self . _PER_PAGE )
return self . playlist_result ( entries , user_id , f ' { user_id } - { user_type } ' )