├── .gitattributes ├── .github └── workflows │ └── main.yml ├── README.md ├── iptv.py ├── lives.m3u └── lives.txt /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: update-lives-list 2 | 3 | on: 4 | schedule: 5 | - cron: '0 21 * * *' 6 | push: 7 | branches: 8 | - main 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v2 17 | 18 | - name: Set up Python 19 | uses: actions/setup-python@v2 20 | with: 21 | python-version: 3.x 22 | 23 | - name: Install dependencies 24 | run: pip install selenium requests futures eventlet 25 | 26 | - name: Run iptv 27 | run: python ${{ github.workspace }}/iptv.py 28 | 29 | 30 | - name: 提交 31 | run: | 32 | git config --local user.email "ssili@126.com" 33 | git config --local user.name "ssili126" 34 | git add . 35 | git commit *.txt -m "Add generated file" 36 | git commit *.m3u -m "Add generated file" 37 | #git pull --rebase 38 | git push -f 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AutoIPTV-Hotel 自动采集整理。 2 | ### 声明: 3 | 4 | 1.所有内容均来自与互联网公开资源,仓库脚本仅参与收集和整理。 5 | 6 | 2.仓库订阅服务仅供开发者自用,从未邀请任何人或组织订阅。 7 | 8 | 3.所有内容未经人工审核,不排除某些内容可能会引起您的不适。 9 | 10 | 4.每天自动更新,侵权请联系: mail@ntvbox.eu.org 11 | 12 | 5.若你不能接受以上声明,您应该立即停止有关于本仓库的所有操作。 13 | 14 | ### 订阅: 15 | 16 | github订阅: 17 | 18 | https://raw.githubusercontent.com/cymz6/AutoIPTV-Hotel/main/lives.txt 19 | 20 | https://raw.githubusercontent.com/cymz6/AutoIPTV-Hotel/main/lives.m3u 21 | 22 | github代理订阅: 23 | 24 | https://ghproxy.net/https://raw.githubusercontent.com/cymz6/AutoIPTV-Hotel/main/lives.txt 25 | 26 | https://ghproxy.net/https://raw.githubusercontent.com/cymz6/AutoIPTV-Hotel/main/lives.m3u 27 | 28 | ### 鸣谢: 29 | 30 | 脚本来源: https://github.com/ssili126/tv 31 | -------------------------------------------------------------------------------- /iptv.py: -------------------------------------------------------------------------------- 1 | import time 2 | import concurrent.futures 3 | from selenium import webdriver 4 | from selenium.webdriver.chrome.options import Options 5 | import requests 6 | import re 7 | import os 8 | import threading 9 | from queue import Queue 10 | import eventlet 11 | eventlet.monkey_patch() 12 | 13 | urls = [ 14 | "https://fofa.info/result?qbase64=ImlwdHYvbGl2ZS96aF9jbi5qcyIgJiYgY291bnRyeT0iQ04iICYmIHJlZ2lvbj0iU2ljaHVhbiI%3D", # 四川 15 | "https://fofa.info/result?qbase64=ImlwdHYvbGl2ZS96aF9jbi5qcyIgJiYgY291bnRyeT0iQ04iICYmIHJlZ2lvbj0i5LqR5Y2XIg%3D%3D", # 云南 16 | "https://fofa.info/result?qbase64=ImlwdHYvbGl2ZS96aF9jbi5qcyIgJiYgY291bnRyeT0iQ04iICYmIHJlZ2lvbj0iQ2hvbmdxaW5nIg%3D%3D", # 重庆 17 | "https://fofa.info/result?qbase64=ImlwdHYvbGl2ZS96aF9jbi5qcyIgJiYgY291bnRyeT0iQ04iICYmIHJlZ2lvbj0iR3VpemhvdSI%3D", # 贵州 18 | "https://fofa.info/result?qbase64=ImlwdHYvbGl2ZS96aF9jbi5qcyIgJiYgY291bnRyeT0iQ04iICYmIHJlZ2lvbj0iU2hhbnhpIg%3D%3D", # 山西 19 | "https://fofa.info/result?qbase64=ImlwdHYvbGl2ZS96aF9jbi5qcyIgJiYgY291bnRyeT0iQ04iICYmIHJlZ2lvbj0iR3Vhbmd4aSBaaHVhbmd6dSI%3D", # 广西 20 | 21 | 22 | ] 23 | 24 | def modify_urls(url): 25 | modified_urls = [] 26 | ip_start_index = url.find("//") + 2 27 | ip_end_index = url.find(":", ip_start_index) 28 | base_url = url[:ip_start_index] # http:// or https:// 29 | ip_address = url[ip_start_index:ip_end_index] 30 | port = url[ip_end_index:] 31 | ip_end = "/iptv/live/1000.json?key=txiptv" 32 | for i in range(1, 256): 33 | modified_ip = f"{ip_address[:-1]}{i}" 34 | modified_url = f"{base_url}{modified_ip}{port}{ip_end}" 35 | modified_urls.append(modified_url) 36 | 37 | return modified_urls 38 | 39 | 40 | def is_url_accessible(url): 41 | try: 42 | response = requests.get(url, timeout=0.5) 43 | if response.status_code == 200: 44 | return url 45 | except requests.exceptions.RequestException: 46 | pass 47 | return None 48 | 49 | 50 | results = [] 51 | 52 | for url in urls: 53 | # 创建一个Chrome WebDriver实例 54 | chrome_options = Options() 55 | chrome_options.add_argument('--headless') 56 | chrome_options.add_argument('--no-sandbox') 57 | chrome_options.add_argument('--disable-dev-shm-usage') 58 | 59 | driver = webdriver.Chrome(options=chrome_options) 60 | # 使用WebDriver访问网页 61 | driver.get(url) # 将网址替换为你要访问的网页地址 62 | time.sleep(10) 63 | # 获取网页内容 64 | page_content = driver.page_source 65 | 66 | # 关闭WebDriver 67 | driver.quit() 68 | 69 | # 查找所有符合指定格式的网址 70 | pattern = r"http://\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d+" # 设置匹配的格式,如http://8.8.8.8:8888 71 | urls_all = re.findall(pattern, page_content) 72 | # urls = list(set(urls_all)) # 去重得到唯一的URL列表 73 | urls = set(urls_all) # 去重得到唯一的URL列表 74 | x_urls = [] 75 | for url in urls: # 对urls进行处理,ip第四位修改为1,并去重 76 | url = url.strip() 77 | ip_start_index = url.find("//") + 2 78 | ip_end_index = url.find(":", ip_start_index) 79 | ip_dot_start = url.find(".") + 1 80 | ip_dot_second = url.find(".", ip_dot_start) + 1 81 | ip_dot_three = url.find(".", ip_dot_second) + 1 82 | base_url = url[:ip_start_index] # http:// or https:// 83 | ip_address = url[ip_start_index:ip_dot_three] 84 | port = url[ip_end_index:] 85 | ip_end = "1" 86 | modified_ip = f"{ip_address}{ip_end}" 87 | x_url = f"{base_url}{modified_ip}{port}" 88 | x_urls.append(x_url) 89 | urls = set(x_urls) # 去重得到唯一的URL列表 90 | 91 | valid_urls = [] 92 | # 多线程获取可用url 93 | with concurrent.futures.ThreadPoolExecutor(max_workers=100) as executor: 94 | futures = [] 95 | for url in urls: 96 | url = url.strip() 97 | modified_urls = modify_urls(url) 98 | for modified_url in modified_urls: 99 | futures.append(executor.submit(is_url_accessible, modified_url)) 100 | 101 | for future in concurrent.futures.as_completed(futures): 102 | result = future.result() 103 | if result: 104 | valid_urls.append(result) 105 | 106 | for url in valid_urls: 107 | print(url) 108 | # 遍历网址列表,获取JSON文件并解析 109 | for url in valid_urls: 110 | try: 111 | # 发送GET请求获取JSON文件,设置超时时间为0.5秒 112 | ip_start_index = url.find("//") + 2 113 | ip_dot_start = url.find(".") + 1 114 | ip_index_second = url.find("/", ip_dot_start) 115 | base_url = url[:ip_start_index] # http:// or https:// 116 | ip_address = url[ip_start_index:ip_index_second] 117 | url_x = f"{base_url}{ip_address}" 118 | 119 | json_url = f"{url}" 120 | response = requests.get(json_url, timeout=0.5) 121 | json_data = response.json() 122 | 123 | try: 124 | # 解析JSON文件,获取name和url字段 125 | for item in json_data['data']: 126 | if isinstance(item, dict): 127 | name = item.get('name') 128 | urlx = item.get('url') 129 | if ',' in urlx: 130 | urlx=f"aaaaaaaa" 131 | #if 'http' in urlx or 'udp' in urlx or 'rtp' in urlx: 132 | if 'http' in urlx: 133 | urld = f"{urlx}" 134 | else: 135 | urld = f"{url_x}{urlx}" 136 | 137 | if name and urlx: 138 | # 删除特定文字 139 | name = name.replace("cctv", "CCTV") 140 | name = name.replace("中央", "CCTV") 141 | name = name.replace("央视", "CCTV") 142 | name = name.replace("高清", "") 143 | name = name.replace("超高", "") 144 | name = name.replace("HD", "") 145 | name = name.replace("标清", "") 146 | name = name.replace("频道", "") 147 | name = name.replace("-", "") 148 | name = name.replace(" ", "") 149 | name = name.replace("PLUS", "+") 150 | name = name.replace("+", "+") 151 | name = name.replace("(", "") 152 | name = name.replace(")", "") 153 | name = re.sub(r"CCTV(\d+)台", r"CCTV\1", name) 154 | name = name.replace("CCTV1综合", "CCTV1") 155 | name = name.replace("CCTV2财经", "CCTV2") 156 | name = name.replace("CCTV3综艺", "CCTV3") 157 | name = name.replace("CCTV4国际", "CCTV4") 158 | name = name.replace("CCTV4中文国际", "CCTV4") 159 | name = name.replace("CCTV4欧洲", "CCTV4") 160 | name = name.replace("CCTV5体育", "CCTV5") 161 | name = name.replace("CCTV6电影", "CCTV6") 162 | name = name.replace("CCTV7军事", "CCTV7") 163 | name = name.replace("CCTV7军农", "CCTV7") 164 | name = name.replace("CCTV7农业", "CCTV7") 165 | name = name.replace("CCTV7国防军事", "CCTV7") 166 | name = name.replace("CCTV8电视剧", "CCTV8") 167 | name = name.replace("CCTV9记录", "CCTV9") 168 | name = name.replace("CCTV9纪录", "CCTV9") 169 | name = name.replace("CCTV10科教", "CCTV10") 170 | name = name.replace("CCTV11戏曲", "CCTV11") 171 | name = name.replace("CCTV12社会与法", "CCTV12") 172 | name = name.replace("CCTV13新闻", "CCTV13") 173 | name = name.replace("CCTV新闻", "CCTV13") 174 | name = name.replace("CCTV14少儿", "CCTV14") 175 | name = name.replace("CCTV15音乐", "CCTV15") 176 | name = name.replace("CCTV16奥林匹克", "CCTV16") 177 | name = name.replace("CCTV17农业农村", "CCTV17") 178 | name = name.replace("CCTV17农业", "CCTV17") 179 | name = name.replace("CCTV5+体育赛视", "CCTV5+") 180 | name = name.replace("CCTV5+体育赛事", "CCTV5+") 181 | name = name.replace("CCTV5+体育", "CCTV5+") 182 | results.append(f"{name},{urld}") 183 | except: 184 | continue 185 | except: 186 | continue 187 | 188 | 189 | channels = [] 190 | 191 | for result in results: 192 | line = result.strip() 193 | if result: 194 | channel_name, channel_url = result.split(',') 195 | channels.append((channel_name, channel_url)) 196 | 197 | # 线程安全的队列,用于存储下载任务 198 | task_queue = Queue() 199 | 200 | # 线程安全的列表,用于存储结果 201 | results = [] 202 | 203 | error_channels = [] 204 | 205 | 206 | # 定义工作线程函数 207 | def worker(): 208 | while True: 209 | # 从队列中获取一个任务 210 | channel_name, channel_url = task_queue.get() 211 | try: 212 | channel_url_t = channel_url.rstrip(channel_url.split('/')[-1]) # m3u8链接前缀 213 | lines = requests.get(channel_url, timeout = 1).text.strip().split('\n') # 获取m3u8文件内容 214 | ts_lists = [line.split('/')[-1] for line in lines if line.startswith('#') == False] # 获取m3u8文件下视频流后缀 215 | ts_lists_0 = ts_lists[0].rstrip(ts_lists[0].split('.ts')[-1]) # m3u8链接前缀 216 | ts_url = channel_url_t + ts_lists[0] # 拼接单个视频片段下载链接 217 | 218 | # 多获取的视频数据进行5秒钟限制 219 | with eventlet.Timeout(5, False): 220 | start_time = time.time() 221 | content = requests.get(ts_url, timeout = 1).content 222 | end_time = time.time() 223 | response_time = (end_time - start_time) * 1 224 | 225 | if content: 226 | with open(ts_lists_0, 'ab') as f: 227 | f.write(content) # 写入文件 228 | file_size = len(content) 229 | # print(f"文件大小:{file_size} 字节") 230 | download_speed = file_size / response_time / 1024 231 | # print(f"下载速度:{download_speed:.3f} kB/s") 232 | normalized_speed = min(max(download_speed / 1024, 0.001), 100) # 将速率从kB/s转换为MB/s并限制在1~100之间 233 | #print(f"标准化后的速率:{normalized_speed:.3f} MB/s") 234 | 235 | # 删除下载的文件 236 | os.remove(ts_lists_0) 237 | result = channel_name, channel_url, f"{normalized_speed:.3f} MB/s" 238 | results.append(result) 239 | numberx = (len(results) + len(error_channels)) / len(channels) * 100 240 | print(f"可用频道:{len(results)} 个 , 不可用频道:{len(error_channels)} 个 , 总频道:{len(channels)} 个 ,总进度:{numberx:.2f} %。") 241 | except: 242 | error_channel = channel_name, channel_url 243 | error_channels.append(error_channel) 244 | numberx = (len(results) + len(error_channels)) / len(channels) * 100 245 | print(f"可用频道:{len(results)} 个 , 不可用频道:{len(error_channels)} 个 , 总频道:{len(channels)} 个 ,总进度:{numberx:.2f} %。") 246 | 247 | # 标记任务完成 248 | task_queue.task_done() 249 | 250 | 251 | # 创建多个工作线程 252 | num_threads = 10 253 | for _ in range(num_threads): 254 | t = threading.Thread(target=worker, daemon=True) # 将工作线程设置为守护线程 255 | t.start() 256 | 257 | # 添加下载任务到队列 258 | for channel in channels: 259 | task_queue.put(channel) 260 | 261 | # 等待所有任务完成 262 | task_queue.join() 263 | 264 | 265 | def channel_key(channel_name): 266 | match = re.search(r'\d+', channel_name) 267 | if match: 268 | return int(match.group()) 269 | else: 270 | return float('inf') # 返回一个无穷大的数字作为关键字 271 | 272 | # 对频道进行排序 273 | results.sort(key=lambda x: (x[0], -float(x[2].split()[0]))) 274 | results.sort(key=lambda x: channel_key(x[0])) 275 | 276 | 277 | result_counter = 8 # 每个频道需要的个数 278 | 279 | with open("lives.txt", 'w', encoding='utf-8') as file: 280 | channel_counters = {} 281 | file.write('央视频道,#genre#\n') 282 | for result in results: 283 | channel_name, channel_url, speed = result 284 | if 'CCTV' in channel_name: 285 | if channel_name in channel_counters: 286 | if channel_counters[channel_name] >= result_counter: 287 | continue 288 | else: 289 | file.write(f"{channel_name},{channel_url}\n") 290 | channel_counters[channel_name] += 1 291 | else: 292 | file.write(f"{channel_name},{channel_url}\n") 293 | channel_counters[channel_name] = 1 294 | channel_counters = {} 295 | file.write('卫视频道,#genre#\n') 296 | for result in results: 297 | channel_name, channel_url, speed = result 298 | if '卫视' in channel_name: 299 | if channel_name in channel_counters: 300 | if channel_counters[channel_name] >= result_counter: 301 | continue 302 | else: 303 | file.write(f"{channel_name},{channel_url}\n") 304 | channel_counters[channel_name] += 1 305 | else: 306 | file.write(f"{channel_name},{channel_url}\n") 307 | channel_counters[channel_name] = 1 308 | channel_counters = {} 309 | file.write('其他频道,#genre#\n') 310 | for result in results: 311 | channel_name, channel_url, speed = result 312 | if 'CCTV' not in channel_name and '卫视' not in channel_name and '测试' not in channel_name: 313 | if channel_name in channel_counters: 314 | if channel_counters[channel_name] >= result_counter: 315 | continue 316 | else: 317 | file.write(f"{channel_name},{channel_url}\n") 318 | channel_counters[channel_name] += 1 319 | else: 320 | file.write(f"{channel_name},{channel_url}\n") 321 | channel_counters[channel_name] = 1 322 | 323 | with open("lives.m3u", 'w', encoding='utf-8') as file: 324 | channel_counters = {} 325 | file.write('#EXTM3U\n') 326 | for result in results: 327 | channel_name, channel_url, speed = result 328 | if 'CCTV' in channel_name: 329 | if channel_name in channel_counters: 330 | if channel_counters[channel_name] >= result_counter: 331 | continue 332 | else: 333 | file.write(f"#EXTINF:-1 group-title=\"央视频道\",{channel_name}\n") 334 | file.write(f"{channel_url}\n") 335 | channel_counters[channel_name] += 1 336 | else: 337 | file.write(f"#EXTINF:-1 group-title=\"央视频道\",{channel_name}\n") 338 | file.write(f"{channel_url}\n") 339 | channel_counters[channel_name] = 1 340 | channel_counters = {} 341 | #file.write('卫视频道,#genre#\n') 342 | for result in results: 343 | channel_name, channel_url, speed = result 344 | if '卫视' in channel_name: 345 | if channel_name in channel_counters: 346 | if channel_counters[channel_name] >= result_counter: 347 | continue 348 | else: 349 | file.write(f"#EXTINF:-1 group-title=\"卫视频道\",{channel_name}\n") 350 | file.write(f"{channel_url}\n") 351 | channel_counters[channel_name] += 1 352 | else: 353 | file.write(f"#EXTINF:-1 group-title=\"卫视频道\",{channel_name}\n") 354 | file.write(f"{channel_url}\n") 355 | channel_counters[channel_name] = 1 356 | channel_counters = {} 357 | #file.write('其他频道,#genre#\n') 358 | for result in results: 359 | channel_name, channel_url, speed = result 360 | if 'CCTV' not in channel_name and '卫视' not in channel_name and '测试' not in channel_name: 361 | if channel_name in channel_counters: 362 | if channel_counters[channel_name] >= result_counter: 363 | continue 364 | else: 365 | file.write(f"#EXTINF:-1 group-title=\"其他频道\",{channel_name}\n") 366 | file.write(f"{channel_url}\n") 367 | channel_counters[channel_name] += 1 368 | else: 369 | file.write(f"#EXTINF:-1 group-title=\"其他频道\",{channel_name}\n") 370 | file.write(f"{channel_url}\n") 371 | channel_counters[channel_name] = 1 372 | 373 | -------------------------------------------------------------------------------- /lives.m3u: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | -------------------------------------------------------------------------------- /lives.txt: -------------------------------------------------------------------------------- 1 | 央视频道,#genre# 2 | 卫视频道,#genre# 3 | 其他频道,#genre# 4 | --------------------------------------------------------------------------------