import re
import time
import datetime
import requests
import gzip
# 鉴权验证
def getValidAuthenticationHWCTC():
    session = requests.Session()
    #$$$鉴权验证，提交的数据抓包获得，需要改成你自己的
    form_data = {"UserID":"ip15513896894@itv", "Lang":"1", "SupportHD":"1", "NetUserID":"", "Authenticator":"9E6615E19E832F2CC6BD4272D249C017F3467A2C4BA32B437B0196DAE0B4B5D94A857A4767B25D4BA1943739E495EBBBD9BB1A80607A020F9ED4975C881BFAC36A14C6520D837B89AF3028D559E8839C2490B25081883DEAF421ED11521B3E3F2EB4ABE769932911C6F1EC46F184B0FB319518DB19497539AA78CEFB5538A08F", "STBType":"EC6108V9U_pub_hbjdx", "STBVersion":"V100R005C10LHED03SPC002B011", "conntype":"4", "STBID":"001003990060689015455493593FD71F", "templateName":"hbdxggpt", "areaId":"8130335", "userToken":"573917B51460505F7646F26B209DA124", "userGroupId":"1", "productPackageId":"-1", "mac":"54:93:59:3F:D7:1F", "UserField":"2","SoftwareVersion":"V100R003C82LHED01B015", "IsSmartStb":"0", "desktopId":"","stbmaker":"", "XMPPCapability":"1", "ChipID":"","VIP":""}
    response = session.post(IPTVhost+'/EPG/jsp/ValidAuthenticationHWCTC.jsp',data=form_data)
    #返回经过验证的会话
    return session
#根据台号确定是否重新验证，也就是userchannelID
def ReValidAuthentication(NOs):
    res = False
    if ((NOs=='1')or(NOs=='101')or(NOs=='201')or(NOs=='401')or(NOs=='501')or(NOs=='601')or(NOs=='701')or(NOs=='801')or(NOs=='901')):
        res = True    
    return res
#根据台号进行分组，返回分组名称
def getgrouptitle(NOs):
    if int(NOs, base=10) < 20:
        group_title = '央视台'
    elif int(NOs, base=10) < 130:
        group_title = '河北省台'
    elif int(NOs, base=10) < 200:
        group_title = '河北市级台'
    elif int(NOs, base=10) < 400:
        group_title = '全国卫视'
    elif int(NOs, base=10) < 500:
        group_title = '数字标清'
    elif int(NOs, base=10) < 600:
        group_title = '数字高清'
    elif int(NOs, base=10) < 700:
        group_title = '数字直播'
    elif int(NOs, base=10) < 900:
        group_title = '县级台'
    else:
        group_title = '其他'
    return group_title
#获取频道列表和播放地址
def getchannellist():
    channeldata = [] #构造用于返回的频道数据
    today_zero = int(round(time.mktime(datetime.date.today().timetuple()) * 1000)) #获取今日0点时间戳毫秒  
    #获取频道列表的post表单提交用于得到含有播放地址的数据
    session = getValidAuthenticationHWCTC()
    response = session.post(IPTVhost+'/EPG/jsp/getchannellistHWCTC.jsp')
    #返回的结果字符串
    channel_text = response.text
    # 提取频道名称和链接
    channel_ID = re.findall(r'\bChannelID="(.*?)"', channel_text)
    channel_names = re.findall(r'ChannelName="(.*?)"', channel_text)
    userchannel_ID = re.findall(r'UserChannelID="(.*?)"', channel_text)
    channel_urls = re.findall(r'ChannelURL="(.*?)"', channel_text)
    #对播放地址再次匹配，提取有用数据，并对频道进行分组 
    for channelID, name, userchannelID, url in zip(channel_ID, channel_names, userchannel_ID, channel_urls):
        # 分别保留rtsp和igmp链接，并过滤掉无用的部分
        # 只保留rtsp部分，去掉igmp部分
        igmp_link = re.search(r'igmp://([^\s|]+)', url)  # 匹配igmp链接的ip地址部分
        rtsp_link = re.search(r'(rtsp://[^\s|]+smil)', url)  # 匹配rtsp链接
        #获取channellogo台标  
        #一次鉴权后查询的数据有限最多大概到四百个频道左右，未避免遗漏，每一百个频道重新鉴权验证
        if ReValidAuthentication(userchannelID):
            session = getValidAuthenticationHWCTC()        
        channellogo = getchannellogo(today_zero,channelID,session)
        #根据台号进行分组
        group_title = getgrouptitle(userchannelID)
        if rtsp_link:
            #将相关数据整合
            channeldata.append((channelID, name, userchannelID, channellogo, group_title, rtsp_link.group(1), igmp_link.group(1)))  
    return channeldata
#获取channellogo台标    
def getchannellogo(today_zero,channelID,session):
    channellogo = ""
    #构造用于查询频道logo的文件头
    headers= {"Content-Type":"application/x-www-form-urlencoded; charset=UTF-8", "X-Requested-With":"XMLHttpRequest",}
    #构造提交的表单数据，抓包获得
    data = {
        "queryChannel":{
            "channelIDs":[channelID],
            "offset":0},
        "queryPlaybillContext":{
            "date":today_zero,
            "type":1,
            "preNumber":1,
            "nextNumber":3}}    
    response = session.post(IPTVhost+'/VSP/V3/QueryPlaybillContext', headers=headers, json=data)
    #通过正则表达式筛选出logo地址
    channellogores = re.search(r'(http://[^"^\s]*\.(png|jpg|gif))', response.text)
    if channellogores:
       channellogo = channellogores.group(1)    
    return channellogo
#获取节目单数据
def getplaylist(channeldata):
    #用于返回的数据
    playlistdata = []
    #六天之前的凌晨，一天时间为86400000，六天为518400000
    limittime = int(round(time.mktime(datetime.date.today().timetuple()) * 1000)) - 518400000 #支持的最早时间六天前0点
    headers= {"Content-Type":"application/x-www-form-urlencoded; charset=UTF-8", "X-Requested-With":"XMLHttpRequest",}
    for channelID, name, userchannelID, channellogo, group_title, rtspurl, igmpurl in channeldata:
        #一次鉴权后查询的数据有限最多大概到四百个频道左右，未避免遗漏，每一百个频道重新鉴权验证
        if ReValidAuthentication(userchannelID):
            session = getValidAuthenticationHWCTC()
        Tendaysdata = []
        starttime = limittime
        for once in '12345678':   #能查出10天的节目单，但是APTV不支持，只支持7天回看一天预告
            data = {
                "queryChannel":{
                    "channelIDs":[channelID]},
                "queryPlaybill":{
                    "type":"0",
                    "startTime":starttime,
                    "endTime":starttime + 86400000,
                    "count":"100",
                    "offset":"0",
                    "isFillProgram":"0",
                    "mustIncluded":"0"},
                "needChannel":"0"}         
            response = session.post(IPTVhost+'/VSP/V3/QueryPlaybillList', headers=headers, json=data)
            starttime += 86400000
            dayplaydata = formatdayplaydata(response.text)
            Tendaysdata.append((dayplaydata))                
        playlistdata.append((channelID,userchannelID,name,Tendaysdata))
    return playlistdata
#从返回的节目单字符串中提取相关数据
def formatdayplaydata(playlisttext):
    #用于返回的数据    
    dayplaydata = [] 
    ##匹配出一天的节目单
    clean_text = re.findall(r'(startTime":".*?endTime":".*?)"', playlisttext)  
    #将节目单中的<>符号替换为《》，以免被错误解读，这是替换表
    trantab = str.maketrans({'<': '《', '>': '》'})
    for playlist_text in clean_text:
        channelID = re.search(r'channelID":"(.*?)"', playlist_text)  # 匹配频道ID
        starttime_str = re.search(r'startTime":"(.*?)"', playlist_text)  # 匹配开始时间
        name_str = re.search(r'name":"(.*?)"', playlist_text)  # 匹配节目名       
        endtime_str = re.search(r'endTime":"(.*)', playlist_text)  # 匹配结束时间
        #将开始时间和结束时间转换为时移代码    
        #startTime = time.strftime("%Y%m%d%H%M%S", time.localtime(int(starttime_str.group(1))/1000))
        #endTime = time.strftime("%Y%m%d%H%M%S", time.localtime(int(endtime_str.group(1))/1000))
        #对节目名称进行符号替换
        if name_str:
            name = name_str.group(1).translate(trantab)
        #将开始时间和结束时间转换为时移代码 
        if starttime_str:
            startTime = time.strftime("%Y%m%d%H%M%S", time.localtime(int(starttime_str.group(1))/1000)) + " +0800"
        if endtime_str:    
            endTime = time.strftime("%Y%m%d%H%M%S", time.localtime(int(endtime_str.group(1))/1000)) + " +0800"
        dayplaydata.append((channelID.group(1),startTime,name,endTime))        
    return dayplaydata 
# 生成带有rtsp地址可回看的M3U文件
def generate_rtspm3u(channeldata, rtspreplay_path,):
    with open(rtspreplay_path, 'w', encoding='utf-8') as rtspfile:
        rtspfile.write('#EXTM3U\n')  # M3U文件头
        # 写入已排序的频道信息
        for channelID, name, userchannelID, channellogo, group_title, rtspurl, igmpurl in channeldata:            
            # 写入频道信息和RTSP链接，按照指定格式
            rtspfile.write(f'#EXTINF:-1 tvg-id="{userchannelID}" tvg-name="{name}" tvg-logo="{channellogo}" group-title="{group_title}", {name}\n')
            rtspfile.write(f'{rtspurl}\n')  # 空白行分隔  
    print(f'M3U文件已生成：{rtspreplay_path}')
# 生成带有内网IP地址的M3U文件仅可用于直播，不可回看
def generate_httpm3u(channeldata, httplive_path):
    with open(httplive_path, 'w', encoding='utf-8') as httpfile:
        httpfile.write('#EXTM3U\n')  # M3U文件头
        # 写入已排序的频道信息
        for channelID,name, userchannelID, channellogo, group_title, rtspurl, igmpurl in channeldata:
            # 写入频道信息和RTSP链接，按照指定格式
            httpurl = udphost + "/udp/" + igmpurl
            httpfile.write(f'#EXTINF:-1 tvg-id="{userchannelID}" tvg-name="{name}" tvg-logo="{channellogo}" group-title="{group_title}", {name}\n')
            httpfile.write(f'{httpurl}\n')  # 空白行分隔
    print(f'M3U文件已生成：{httplive_path}')
# 生成带有域名地址的M3U文件，可用于外网访问直播，主路由需要开启相关端口映射，内网使用也可但不推荐，不可回看
def generate_ddnsm3u(channeldata, ddnslive_path):
    with open(ddnslive_path, 'w', encoding='utf-8') as ddnsfile:
        ddnsfile.write('#EXTM3U\n')  # M3U文件头
        # 写入频道信息
        for channelID, name, userchannelID, channellogo, group_title, rtspurl, igmpurl in channeldata:
            ddnsurl = ddnshost + "/udp/" + igmpurl
            # 写入频道信息和RTSP链接，按照指定格式
            ddnsfile.write(f'#EXTINF:-1 tvg-id="{userchannelID}" tvg-name="{name}" tvg-logo="{channellogo}" group-title="{group_title}", {name}\n')
            ddnsfile.write(f'{ddnsurl}\n')  # 空白行分隔
    print(f'M3U文件已生成：{ddnslive_path}')
def generate_replaym3u(channeldata, ddnsreplay_path):
    with open(ddnsreplay_path, 'w', encoding='utf-8') as ddnsfile:
        ddnsfile.write('#EXTM3U\n')  # M3U文件头
        # 写入频道信息
        for channelID, name, userchannelID, channellogo, group_title, rtspurl, igmpurl in channeldata:
            suffix_link = re.search(r'(/PLTV/[^\s|]+smil)', rtspurl)  # 去掉rtsp及ip地址部分
            replayurl = replayhost + suffix_link.group(1)
            # 写入频道信息和RTSP链接，按照指定格式
            ddnsfile.write(f'#EXTINF:-1 tvg-id="{userchannelID}" tvg-name="{name}" tvg-logo="{channellogo}" group-title="{group_title}", {name}\n')
            ddnsfile.write(f'{replayurl}\n')  # 空白行分隔
    print(f'M3U文件已生成：{ddnsreplay_path}')
# 生成epg电子节目单文件，节目单文件可以配合rtsp地址进行回看，直播也可看到节目预告
def generate_playlist(playdata, playlist_path):
    with open(playlist_path, 'w', encoding='utf-8') as playlistfile:
        playlistfile.write('<?xml version="1.0" encoding="UTF-8"?>\n')  # M3U文件头
        playlistfile.write('<tv info-name="河北电信EPG" info-url="">\n')  # M3U文件头
        for channelID, userchannelID, name, Tendaysdata in playdata:
            playlistfile.write(f'<channel id="{userchannelID}">\n<display-name lang="zh">{name}</display-name>\n</channel>\n')
            for dayplaydata in Tendaysdata:
                for channelID,startTime,name,endTime in dayplaydata:
                    playlistfile.write(f'<programme channel="{userchannelID}" start="{startTime}" stop="{endTime}">\n<title lang="zh">{name}</title>\n</programme>\n')
        playlistfile.write('</tv>')
    print(f'xml文件已生成：{playlist_path}')
def generate_playlistgz(playlist_path,playlistgz_path):
    with open(playlist_path, 'r', encoding='utf-8') as playlistfile:
        playlistdata = playlistfile.read()
    with gzip.open(playlistgz_path,"wb") as playlistgzfile:
        playlistgzfile.write(playlistdata.encode('utf-8'))
    print(f'gz文件已生成：{playlistgz_path}')
#程序入口
if __name__ == '__main__':
    #$$$必改项！！！鉴权验证，频道地址，频道logo，节目单的服务器地址，需要抓包改成你自己的
    IPTVhost = 'http://192.168.64.141:33200'
    #给内网提供直播服务的主机IP，也就是软路由的地址，udpx本程序不提供
    udphost = 'http://192.168.9.1:4022'
    #给外网提供直播的端口映射的主机的域名和端口号
    ddnshost = 'http://lbgden3.yyboxdns.com:12312'
    #给外网提供回看的端口映射的主机的域名和端口号
    replayhost = 'rtsp://lbgden3.yyboxdns.com:554'
    ######节目单和播放地址列表文件可以改成你自己的，路径别动
    playlist_path = r'/ql/data/scripts/iptv/out/PL.xml'  # 输出节目单路径
    playlistgz_path = r'/ql/data/scripts/iptv/out/PL.xml.gz'  # 输出节目单路径
    #用于生成不同类型的播放列表的文件路径
    rtspreplay_path = r'/ql/data/scripts/iptv/out/LanReplay.m3u'  # 输出M3U文件路径
    httplive_path = r'/ql/data/scripts/iptv/out/LanLive.m3u' 
    ddnslive_path = r'/ql/data/scripts/iptv/out/NetLive.m3u'
    ddnsreplay_path = r'/ql/data/scripts/iptv/out/NetReplay.m3u'
    ###以上是一些参数设定，可根据需要修改,此外鉴权的部分，也要根据自己抓包修改
    ###################################################################
    #提取频道数据及地址
    channeldata = getchannellist()
    # 根据频道数据并生成M3U文件
    generate_rtspm3u(channeldata, rtspreplay_path)
    generate_httpm3u(channeldata, httplive_path)
    generate_ddnsm3u(channeldata, ddnslive_path)
    generate_replaym3u(channeldata, ddnsreplay_path)
    #提取节目单数据
    playlistdata = getplaylist(channeldata)
    # 根据频道数据并生成xml文件
    generate_playlist(playlistdata, playlist_path)
    generate_playlistgz(playlist_path,playlistgz_path)
