channel.py 40 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084
  1. import asyncio
  2. import base64
  3. import gzip
  4. import json
  5. import os
  6. import pickle
  7. import re
  8. from collections import defaultdict
  9. from logging import INFO
  10. from bs4 import NavigableString
  11. import utils.constants as constants
  12. from updates.epg.tools import write_to_xml, compress_to_gz
  13. from utils.alias import Alias
  14. from utils.config import config
  15. from utils.db import get_db_connection, return_db_connection
  16. from utils.ip_checker import IPChecker
  17. from utils.speed import (
  18. get_speed,
  19. get_speed_result,
  20. get_sort_result,
  21. check_ffmpeg_installed_status,
  22. logger as speed_test_logger
  23. )
  24. from utils.tools import (
  25. format_name,
  26. get_name_url,
  27. check_url_by_keywords,
  28. get_total_urls,
  29. add_url_info,
  30. resource_path,
  31. get_urls_from_file,
  32. get_name_urls_from_file,
  33. get_logger,
  34. get_datetime_now,
  35. get_url_host,
  36. check_ipv_type_match,
  37. get_ip_address,
  38. convert_to_m3u,
  39. custom_print,
  40. get_name_uri_from_dir, get_resolution_value
  41. )
  42. from utils.types import ChannelData, OriginType, CategoryChannelData, TestResult
  43. channel_alias = Alias()
  44. ip_checker = IPChecker()
  45. frozen_channels = set()
  46. location_list = config.location
  47. isp_list = config.isp
  48. max_delay = config.speed_test_timeout * 1000
  49. min_resolution_value = config.min_resolution_value
  50. def format_channel_data(url: str, origin: OriginType) -> ChannelData:
  51. """
  52. Format the channel data
  53. """
  54. url_partition = url.partition("$")
  55. url = url_partition[0]
  56. info = url_partition[2]
  57. if info and info.startswith("!"):
  58. origin = "whitelist"
  59. info = info[1:]
  60. return {
  61. "id": hash(url),
  62. "url": url,
  63. "host": get_url_host(url),
  64. "origin": origin,
  65. "ipv_type": None,
  66. "extra_info": info
  67. }
  68. def check_channel_need_frozen(info: TestResult) -> bool:
  69. """
  70. Check if the channel need to be frozen
  71. """
  72. delay = info.get("delay", 0)
  73. if (delay == -1 or delay > max_delay) or info.get("speed", 0) == 0:
  74. return True
  75. if info.get("resolution"):
  76. if get_resolution_value(info["resolution"]) < min_resolution_value:
  77. return True
  78. return False
  79. def get_channel_data_from_file(channels, file, whitelist, open_local=config.open_local,
  80. local_data=None, live_data=None, hls_data=None) -> CategoryChannelData:
  81. """
  82. Get the channel data from the file
  83. """
  84. current_category = ""
  85. for line in file:
  86. line = line.strip()
  87. if "#genre#" in line:
  88. current_category = line.partition(",")[0]
  89. else:
  90. name_url = get_name_url(
  91. line, pattern=constants.demo_txt_pattern, check_url=False
  92. )
  93. if name_url and name_url[0]:
  94. name = name_url[0]["name"]
  95. url = name_url[0]["url"]
  96. category_dict = channels[current_category]
  97. if name not in category_dict:
  98. category_dict[name] = []
  99. if name in whitelist:
  100. for whitelist_url in whitelist[name]:
  101. category_dict[name].append(format_channel_data(whitelist_url, "whitelist"))
  102. if live_data and name in live_data:
  103. for live_url in live_data[name]:
  104. category_dict[name].append(format_channel_data(live_url, "live"))
  105. if hls_data and name in hls_data:
  106. for hls_url in hls_data[name]:
  107. category_dict[name].append(format_channel_data(hls_url, "hls"))
  108. if open_local:
  109. if url:
  110. category_dict[name].append(format_channel_data(url, "local"))
  111. if local_data:
  112. alias_names = channel_alias.get(name)
  113. alias_names.update([name, format_name(name)])
  114. for alias_name in alias_names:
  115. if alias_name in local_data:
  116. for local_url in local_data[alias_name]:
  117. category_dict[name].append(format_channel_data(local_url, "local"))
  118. elif '*' in alias_name:
  119. pattern = '^' + re.escape(alias_name).replace('\\*', '.*') + '$'
  120. for local_name in local_data:
  121. if re.match(pattern, local_name):
  122. for local_url in local_data[local_name]:
  123. category_dict[name].append(format_channel_data(local_url, "local"))
  124. return channels
  125. def get_channel_items() -> CategoryChannelData:
  126. """
  127. Get the channel items from the source file
  128. """
  129. user_source_file = resource_path(config.source_file)
  130. channels = defaultdict(lambda: defaultdict(list))
  131. live_data = None
  132. hls_data = None
  133. if config.open_rtmp:
  134. live_data = get_name_uri_from_dir(constants.live_path)
  135. hls_data = get_name_uri_from_dir(constants.hls_path)
  136. local_data = get_name_urls_from_file(config.local_file)
  137. whitelist = get_name_urls_from_file(constants.whitelist_path)
  138. whitelist_urls = get_urls_from_file(constants.whitelist_path)
  139. whitelist_len = len(list(whitelist.keys()))
  140. if whitelist_len:
  141. print(f"Found {whitelist_len} channel in whitelist")
  142. if os.path.exists(user_source_file):
  143. with open(user_source_file, "r", encoding="utf-8") as file:
  144. channels = get_channel_data_from_file(
  145. channels, file, whitelist, config.open_local, local_data, live_data, hls_data
  146. )
  147. if config.open_history:
  148. if os.path.exists(constants.cache_path):
  149. try:
  150. with gzip.open(constants.cache_path, "rb") as file:
  151. old_result = pickle.load(file)
  152. for cate, data in channels.items():
  153. if cate in old_result:
  154. for name, info_list in data.items():
  155. urls = [
  156. url
  157. for item in info_list
  158. if (url := item["url"])
  159. ]
  160. if name in old_result[cate]:
  161. channel_data = channels[cate][name]
  162. for info in old_result[cate][name]:
  163. if info:
  164. try:
  165. if check_channel_need_frozen(info):
  166. frozen_channels.add(info["url"])
  167. continue
  168. if info["origin"] == "whitelist" and not any(
  169. url in info["url"] for url in whitelist_urls):
  170. continue
  171. except:
  172. pass
  173. if info["url"] not in urls:
  174. channel_data.append(info)
  175. if not channel_data:
  176. for info in old_result[cate][name]:
  177. if info and info["url"] not in urls:
  178. channel_data.append(info)
  179. frozen_channels.discard(info["url"])
  180. channel_urls = {d["url"] for d in channel_data}
  181. if channel_urls.issubset(frozen_channels):
  182. frozen_channels.difference_update(channel_urls)
  183. except Exception as e:
  184. print(f"Error loading cache file: {e}")
  185. pass
  186. return channels
  187. def format_channel_name(name):
  188. """
  189. Format the channel name with sub and replace and lower
  190. """
  191. return channel_alias.get_primary(name)
  192. def channel_name_is_equal(name1, name2):
  193. """
  194. Check if the channel name is equal
  195. """
  196. name1_format = format_channel_name(name1)
  197. name2_format = format_channel_name(name2)
  198. return name1_format == name2_format
  199. def get_channel_results_by_name(name, data):
  200. """
  201. Get channel results from data by name
  202. """
  203. format_name = format_channel_name(name)
  204. results = data.get(format_name, [])
  205. return results
  206. def get_element_child_text_list(element, child_name):
  207. """
  208. Get the child text of the element
  209. """
  210. text_list = []
  211. children = element.find_all(child_name)
  212. if children:
  213. for child in children:
  214. text = child.get_text(strip=True)
  215. if text:
  216. text_list.append(text)
  217. return text_list
  218. def get_multicast_ip_list(urls):
  219. """
  220. Get the multicast ip list from urls
  221. """
  222. ip_list = []
  223. for url in urls:
  224. pattern = r"rtp://((\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(?::(\d+))?)"
  225. matcher = re.search(pattern, url)
  226. if matcher:
  227. ip_list.append(matcher.group(1))
  228. return ip_list
  229. def get_channel_multicast_region_ip_list(result, channel_region, channel_type):
  230. """
  231. Get the channel multicast region ip list by region and type from result
  232. """
  233. return [
  234. ip
  235. for result_region, result_obj in result.items()
  236. if result_region in channel_region
  237. for url_type, urls in result_obj.items()
  238. if url_type in channel_type
  239. for ip in get_multicast_ip_list(urls)
  240. ]
  241. def get_channel_multicast_name_region_type_result(result, names):
  242. """
  243. Get the multicast name and region and type result by names from result
  244. """
  245. name_region_type_result = {}
  246. for name in names:
  247. data = result.get(name)
  248. if data:
  249. name_region_type_result[name] = data
  250. return name_region_type_result
  251. def get_channel_multicast_region_type_list(result):
  252. """
  253. Get the channel multicast region type list from result
  254. """
  255. region_list = config.multicast_region_list
  256. region_type_list = {
  257. (region, r_type)
  258. for region_type in result.values()
  259. for region, types in region_type.items()
  260. if "all" in region_list
  261. or "ALL" in region_list
  262. or "全部" in region_list
  263. or region in region_list
  264. for r_type in types
  265. }
  266. return list(region_type_list)
  267. def get_channel_multicast_result(result, search_result):
  268. """
  269. Get the channel multicast info result by result and search result
  270. """
  271. info_result = {}
  272. multicast_name = constants.origin_map["multicast"]
  273. for name, result_obj in result.items():
  274. info_list = [
  275. {
  276. "url":
  277. add_url_info(
  278. total_url,
  279. f"{result_region}{result_type}{multicast_name}",
  280. ),
  281. "date": date,
  282. "resolution": resolution,
  283. }
  284. for result_region, result_types in result_obj.items()
  285. if result_region in search_result
  286. for result_type, result_type_urls in result_types.items()
  287. if result_type in search_result[result_region]
  288. for ip in get_multicast_ip_list(result_type_urls) or []
  289. for url, date, resolution in search_result[result_region][result_type]
  290. if (total_url := f"http://{url}/rtp/{ip}")
  291. ]
  292. info_result[name] = info_list
  293. return info_result
  294. def get_results_from_soup(soup, name):
  295. """
  296. Get the results from the soup
  297. """
  298. results = []
  299. if not soup.descendants:
  300. return results
  301. for element in soup.descendants:
  302. if isinstance(element, NavigableString):
  303. text = element.get_text(strip=True)
  304. url = get_channel_url(text)
  305. if url and not any(item[0] == url for item in results):
  306. url_element = soup.find(lambda tag: tag.get_text(strip=True) == url)
  307. if url_element:
  308. name_element = url_element.find_previous_sibling()
  309. if name_element:
  310. channel_name = name_element.get_text(strip=True)
  311. if channel_name_is_equal(name, channel_name):
  312. info_element = url_element.find_next_sibling()
  313. date, resolution = get_channel_info(
  314. info_element.get_text(strip=True)
  315. )
  316. results.append({
  317. "url": url,
  318. "date": date,
  319. "resolution": resolution,
  320. })
  321. return results
  322. def get_results_from_multicast_soup(soup, hotel=False):
  323. """
  324. Get the results from the multicast soup
  325. """
  326. results = []
  327. if not soup.descendants:
  328. return results
  329. for element in soup.descendants:
  330. if isinstance(element, NavigableString):
  331. text = element.strip()
  332. if "失效" in text:
  333. continue
  334. url = get_channel_url(text)
  335. if url and not any(item["url"] == url for item in results):
  336. url_element = soup.find(lambda tag: tag.get_text(strip=True) == url)
  337. if not url_element:
  338. continue
  339. parent_element = url_element.find_parent()
  340. info_element = parent_element.find_all(recursive=False)[-1]
  341. if not info_element:
  342. continue
  343. info_text = info_element.get_text(strip=True)
  344. if "上线" in info_text and " " in info_text:
  345. date, region, channel_type = get_multicast_channel_info(info_text)
  346. if hotel and "酒店" not in region:
  347. continue
  348. results.append(
  349. {
  350. "url": url,
  351. "date": date,
  352. "region": region,
  353. "type": channel_type,
  354. }
  355. )
  356. return results
  357. def get_results_from_soup_requests(soup, name):
  358. """
  359. Get the results from the soup by requests
  360. """
  361. results = []
  362. elements = soup.find_all("div", class_="resultplus") if soup else []
  363. for element in elements:
  364. name_element = element.find("div", class_="channel")
  365. if name_element:
  366. channel_name = name_element.get_text(strip=True)
  367. if channel_name_is_equal(name, channel_name):
  368. text_list = get_element_child_text_list(element, "div")
  369. url = date = resolution = None
  370. for text in text_list:
  371. text_url = get_channel_url(text)
  372. if text_url:
  373. url = text_url
  374. if " " in text:
  375. text_info = get_channel_info(text)
  376. date, resolution = text_info
  377. if url:
  378. results.append({
  379. "url": url,
  380. "date": date,
  381. "resolution": resolution,
  382. })
  383. return results
  384. def get_results_from_multicast_soup_requests(soup, hotel=False):
  385. """
  386. Get the results from the multicast soup by requests
  387. """
  388. results = []
  389. if not soup:
  390. return results
  391. elements = soup.find_all("div", class_="result")
  392. for element in elements:
  393. name_element = element.find("div", class_="channel")
  394. if not name_element:
  395. continue
  396. text_list = get_element_child_text_list(element, "div")
  397. url, date, region, channel_type = None, None, None, None
  398. valid = True
  399. for text in text_list:
  400. if "失效" in text:
  401. valid = False
  402. break
  403. text_url = get_channel_url(text)
  404. if text_url:
  405. url = text_url
  406. if url and "上线" in text and " " in text:
  407. date, region, channel_type = get_multicast_channel_info(text)
  408. if url and valid:
  409. if hotel and "酒店" not in region:
  410. continue
  411. results.append({"url": url, "date": date, "region": region, "type": channel_type})
  412. return results
  413. def get_channel_url(text):
  414. """
  415. Get the url from text
  416. """
  417. url = None
  418. url_search = constants.url_pattern.search(text)
  419. if url_search:
  420. url = url_search.group()
  421. return url
  422. def get_channel_info(text):
  423. """
  424. Get the channel info from text
  425. """
  426. date, resolution = None, None
  427. if text:
  428. date, resolution = (
  429. (text.partition(" ")[0] if text.partition(" ")[0] else None),
  430. (
  431. text.partition(" ")[2].partition("•")[2]
  432. if text.partition(" ")[2].partition("•")[2]
  433. else None
  434. ),
  435. )
  436. return date, resolution
  437. def get_multicast_channel_info(text):
  438. """
  439. Get the multicast channel info from text
  440. """
  441. date, region, channel_type = None, None, None
  442. if text:
  443. text_split = text.split(" ")
  444. filtered_data = list(filter(lambda x: x.strip() != "", text_split))
  445. if filtered_data and len(filtered_data) == 4:
  446. date = filtered_data[0]
  447. region = filtered_data[2]
  448. channel_type = filtered_data[3]
  449. return date, region, channel_type
  450. def init_info_data(data: dict, category: str, name: str) -> None:
  451. """
  452. Initialize channel info data structure if not exists
  453. """
  454. data.setdefault(category, {}).setdefault(name, [])
  455. def append_data_to_info_data(
  456. info_data: dict,
  457. category: str,
  458. name: str,
  459. data: list,
  460. origin: str = None,
  461. check: bool = True,
  462. whitelist: list = None,
  463. blacklist: list = None,
  464. ipv_type_data: dict = None
  465. ) -> None:
  466. """
  467. Append channel data to total info data with deduplication and validation
  468. Args:
  469. info_data: The main data structure to update
  470. category: Category key for the data
  471. name: Name key within the category
  472. data: List of channel items to process
  473. origin: Default origin for items
  474. check: Whether to perform validation checks
  475. whitelist: List of whitelist keywords
  476. blacklist: List of blacklist keywords
  477. ipv_type_data: Dictionary to cache IP type information
  478. """
  479. init_info_data(info_data, category, name)
  480. channel_list = info_data[category][name]
  481. existing_urls = {info["url"] for info in channel_list if "url" in info}
  482. for item in data:
  483. try:
  484. channel_id = item.get("id") or hash(item["url"])
  485. url = item["url"]
  486. host = item.get("host") or get_url_host(url)
  487. date = item.get("date")
  488. delay = item.get("delay")
  489. speed = item.get("speed")
  490. resolution = item.get("resolution")
  491. url_origin = item.get("origin", origin)
  492. ipv_type = item.get("ipv_type")
  493. location = item.get("location")
  494. isp = item.get("isp")
  495. headers = item.get("headers")
  496. catchup = item.get("catchup")
  497. extra_info = item.get("extra_info", "")
  498. if not url_origin or not url:
  499. continue
  500. if (url in frozen_channels or (url in existing_urls and not headers) or
  501. (check and check_url_by_keywords(url, blacklist))):
  502. continue
  503. if not ipv_type:
  504. if ipv_type_data and host in ipv_type_data:
  505. ipv_type = ipv_type_data[host]
  506. else:
  507. ipv_type = ip_checker.get_ipv_type(url)
  508. if ipv_type_data is not None:
  509. ipv_type_data[host] = ipv_type
  510. if check and not check_ipv_type_match(ipv_type):
  511. continue
  512. if not location or not isp:
  513. ip = ip_checker.get_ip(url)
  514. if ip:
  515. location, isp = ip_checker.find_map(ip)
  516. if location and location_list and not any(item in location for item in location_list):
  517. continue
  518. if isp and isp_list and not any(item in isp for item in isp_list):
  519. continue
  520. host_exist = False
  521. if check:
  522. if whitelist and check_url_by_keywords(url, whitelist):
  523. url_origin = "whitelist"
  524. if url_origin not in ["whitelist", "live", "hls"]:
  525. for idx, info in enumerate(channel_list):
  526. if not info.get("url"):
  527. continue
  528. info_host = get_url_host(info["url"])
  529. if info_host == host:
  530. host_exist = True
  531. info_url = info["url"]
  532. # Replace if new URL is longer or has headers
  533. if len(info_url) < len(url) or headers:
  534. if info_url in existing_urls:
  535. existing_urls.remove(info_url)
  536. existing_urls.add(url)
  537. channel_list[idx] = {
  538. "id": channel_id,
  539. "url": url,
  540. "host": host,
  541. "date": date,
  542. "delay": delay,
  543. "speed": speed,
  544. "resolution": resolution,
  545. "origin": origin,
  546. "ipv_type": ipv_type,
  547. "location": location,
  548. "isp": isp,
  549. "headers": headers,
  550. "catchup": catchup,
  551. "extra_info": extra_info
  552. }
  553. break
  554. continue
  555. if not host_exist:
  556. channel_list.append({
  557. "id": channel_id,
  558. "url": url,
  559. "host": host,
  560. "date": date,
  561. "delay": delay,
  562. "speed": speed,
  563. "resolution": resolution,
  564. "origin": url_origin,
  565. "ipv_type": ipv_type,
  566. "location": location,
  567. "isp": isp,
  568. "headers": headers,
  569. "catchup": catchup,
  570. "extra_info": extra_info
  571. })
  572. existing_urls.add(url)
  573. except Exception as e:
  574. print(f"Error processing channel data: {e}")
  575. continue
  576. def get_origin_method_name(method):
  577. """
  578. Get the origin method name
  579. """
  580. return "hotel" if method.startswith("hotel_") else method
  581. def append_old_data_to_info_data(info_data, cate, name, data, whitelist=None, blacklist=None, ipv_type_data=None):
  582. """
  583. Append history and local channel data to total info data
  584. """
  585. append_data_to_info_data(
  586. info_data,
  587. cate,
  588. name,
  589. data,
  590. whitelist=whitelist,
  591. blacklist=blacklist,
  592. ipv_type_data=ipv_type_data
  593. )
  594. live_len = sum(1 for item in data if item["origin"] == "live")
  595. hls_len = sum(1 for item in data if item["origin"] == "hls")
  596. local_len = sum(1 for item in data if item["origin"] == "local")
  597. whitelist_len = sum(1 for item in data if item["origin"] == "whitelist")
  598. history_len = len(data) - (live_len + hls_len + local_len + whitelist_len)
  599. print(f"History: {history_len}, Live: {live_len}, HLS: {hls_len}, Local: {local_len}, Whitelist: {whitelist_len}",
  600. end=", ")
  601. def print_channel_number(data: CategoryChannelData, cate: str, name: str):
  602. """
  603. Print channel number
  604. """
  605. channel_list = data.get(cate, {}).get(name, [])
  606. print("IPv4:", len([channel for channel in channel_list if channel["ipv_type"] == "ipv4"]), end=", ")
  607. print("IPv6:", len([channel for channel in channel_list if channel["ipv_type"] == "ipv6"]), end=", ")
  608. print(
  609. "Total:",
  610. len(channel_list),
  611. )
  612. def append_total_data(
  613. items,
  614. data,
  615. hotel_fofa_result=None,
  616. multicast_result=None,
  617. hotel_foodie_result=None,
  618. subscribe_result=None,
  619. online_search_result=None,
  620. ):
  621. """
  622. Append all method data to total info data
  623. """
  624. total_result = [
  625. ("hotel_fofa", hotel_fofa_result),
  626. ("multicast", multicast_result),
  627. ("hotel_foodie", hotel_foodie_result),
  628. ("subscribe", subscribe_result),
  629. ("online_search", online_search_result),
  630. ]
  631. whitelist = get_urls_from_file(constants.whitelist_path)
  632. blacklist = get_urls_from_file(constants.blacklist_path, pattern_search=False)
  633. url_hosts_ipv_type = {}
  634. open_history = config.open_history
  635. open_local = config.open_local
  636. open_rtmp = config.open_rtmp
  637. for obj in data.values():
  638. for value_list in obj.values():
  639. for value in value_list:
  640. if value_ipv_type := value.get("ipv_type", None):
  641. url_hosts_ipv_type[get_url_host(value["url"])] = value_ipv_type
  642. for cate, channel_obj in items:
  643. for name, old_info_list in channel_obj.items():
  644. print(f"{name}:", end=" ")
  645. if (open_history or open_local or open_rtmp) and old_info_list:
  646. append_old_data_to_info_data(data, cate, name, old_info_list, whitelist=whitelist, blacklist=blacklist,
  647. ipv_type_data=url_hosts_ipv_type)
  648. for method, result in total_result:
  649. if config.open_method[method]:
  650. origin_method = get_origin_method_name(method)
  651. if not origin_method:
  652. continue
  653. name_results = get_channel_results_by_name(name, result)
  654. append_data_to_info_data(
  655. data, cate, name, name_results, origin=origin_method, whitelist=whitelist, blacklist=blacklist,
  656. ipv_type_data=url_hosts_ipv_type
  657. )
  658. print(f"{method.capitalize()}:", len(name_results), end=", ")
  659. print_channel_number(data, cate, name)
  660. async def test_speed(data, ipv6=False, callback=None):
  661. """
  662. Test speed of channel data
  663. """
  664. ipv6_proxy_url = None if (not config.open_ipv6 or ipv6) else constants.ipv6_proxy
  665. open_headers = config.open_headers
  666. get_resolution = config.open_filter_resolution and check_ffmpeg_installed_status()
  667. semaphore = asyncio.Semaphore(config.speed_test_limit)
  668. async def limited_get_speed(channel_info):
  669. """
  670. Wrapper for get_speed with rate limiting
  671. """
  672. async with semaphore:
  673. headers = (open_headers and channel_info.get("headers")) or None
  674. return await get_speed(
  675. channel_info,
  676. headers=headers,
  677. ipv6_proxy=ipv6_proxy_url,
  678. filter_resolution=get_resolution,
  679. callback=callback,
  680. )
  681. tasks = []
  682. channel_map = {}
  683. for cate, channel_obj in data.items():
  684. for name, info_list in channel_obj.items():
  685. for info in info_list:
  686. info['name'] = name
  687. task = asyncio.create_task(limited_get_speed(info))
  688. tasks.append(task)
  689. channel_map[task] = (cate, name, info)
  690. results = await asyncio.gather(*tasks)
  691. speed_test_logger.handlers.clear()
  692. grouped_results = {}
  693. for task, result in zip(tasks, results):
  694. cate, name, info = channel_map[task]
  695. if cate not in grouped_results:
  696. grouped_results[cate] = {}
  697. if name not in grouped_results[cate]:
  698. grouped_results[cate][name] = []
  699. grouped_results[cate][name].append({**info, **result})
  700. return grouped_results
  701. def sort_channel_result(channel_data, result=None, filter_host=False, ipv6_support=True):
  702. """
  703. Sort channel result
  704. """
  705. channel_result = defaultdict(lambda: defaultdict(list))
  706. logger = get_logger(constants.result_log_path, level=INFO, init=True)
  707. for cate, obj in channel_data.items():
  708. for name, values in obj.items():
  709. if not values:
  710. continue
  711. whitelist_result = []
  712. test_result = result.get(cate, {}).get(name, []) if result else []
  713. for value in values:
  714. if value["origin"] in ["whitelist", "live", "hls"] or (
  715. not ipv6_support and result and value["ipv_type"] == "ipv6"
  716. ):
  717. whitelist_result.append(value)
  718. elif filter_host or not result:
  719. test_result.append({**value, **get_speed_result(value["host"])} if filter_host else value)
  720. total_result = whitelist_result + get_sort_result(test_result, ipv6_support=ipv6_support)
  721. append_data_to_info_data(
  722. channel_result,
  723. cate,
  724. name,
  725. total_result,
  726. check=False,
  727. )
  728. for item in total_result:
  729. logger.info(
  730. f"Name: {name}, URL: {item.get('url')}, IPv_Type: {item.get("ipv_type")}, Location: {item.get('location')}, ISP: {item.get('isp')}, Date: {item["date"]}, Delay: {item.get('delay') or -1} ms, Speed: {item.get('speed') or 0:.2f} M/s, Resolution: {item.get('resolution')}"
  731. )
  732. logger.handlers.clear()
  733. return channel_result
  734. def process_write_content(
  735. path: str,
  736. data: CategoryChannelData,
  737. live: bool = False,
  738. hls: bool = False,
  739. live_url: str = None,
  740. hls_url: str = None,
  741. open_empty_category: bool = False,
  742. ipv_type_prefer: list[str] = None,
  743. origin_type_prefer: list[str] = None,
  744. first_channel_name: str = None,
  745. enable_print: bool = False
  746. ):
  747. """
  748. Get channel write content
  749. :param path: write into path
  750. :param live: all live channel url
  751. :param hls: all hls channel url
  752. :param live_url: live url
  753. :param hls_url: hls url
  754. :param open_empty_category: show empty category
  755. :param ipv_type_prefer: ipv type prefer
  756. :param origin_type_prefer: origin type prefer
  757. :param first_channel_name: the first channel name
  758. """
  759. content = ""
  760. no_result_name = []
  761. first_cate = True
  762. result_data = defaultdict(list)
  763. custom_print.disable = not enable_print
  764. rtmp_url = live_url if live else hls_url if hls else None
  765. rtmp_type = ["live", "hls"] if live and hls else ["live"] if live else ["hls"] if hls else []
  766. open_url_info = config.open_url_info
  767. for cate, channel_obj in data.items():
  768. custom_print(f"\n{cate}:", end=" ")
  769. content += f"{'\n\n' if not first_cate else ''}{cate},#genre#"
  770. first_cate = False
  771. channel_obj_keys = channel_obj.keys()
  772. names_len = len(list(channel_obj_keys))
  773. for i, name in enumerate(channel_obj_keys):
  774. info_list = data.get(cate, {}).get(name, [])
  775. channel_urls = get_total_urls(info_list, ipv_type_prefer, origin_type_prefer, rtmp_type)
  776. result_data[name].extend(channel_urls)
  777. end_char = ", " if i < names_len - 1 else ""
  778. custom_print(f"{name}:", len(channel_urls), end=end_char)
  779. if not channel_urls:
  780. if open_empty_category:
  781. no_result_name.append(name)
  782. continue
  783. for item in channel_urls:
  784. item_origin = item.get("origin", None)
  785. item_rtmp_url = None
  786. if item_origin == "live":
  787. item_rtmp_url = live_url
  788. elif item_origin == "hls":
  789. item_rtmp_url = hls_url
  790. item_url = item["url"]
  791. if open_url_info and item["extra_info"]:
  792. item_url = add_url_info(item_url, item["extra_info"])
  793. total_item_url = f"{rtmp_url or item_rtmp_url}{item['id']}" if rtmp_url or item_rtmp_url else item_url
  794. content += f"\n{name},{total_item_url}"
  795. custom_print()
  796. if open_empty_category and no_result_name:
  797. custom_print("\n🈳 No result channel name:")
  798. content += "\n\n🈳无结果频道,#genre#"
  799. for i, name in enumerate(no_result_name):
  800. end_char = ", " if i < len(no_result_name) - 1 else ""
  801. custom_print(name, end=end_char)
  802. content += f"\n{name},url"
  803. custom_print()
  804. if config.open_update_time:
  805. update_time_item = next(
  806. (urls[0] for channel_obj in data.values()
  807. for info_list in channel_obj.values()
  808. if (urls := get_total_urls(info_list, ipv_type_prefer, origin_type_prefer, rtmp_type))),
  809. {"id": "id", "url": "url"}
  810. )
  811. now = get_datetime_now()
  812. update_time_item_url = update_time_item["url"]
  813. if open_url_info and update_time_item["extra_info"]:
  814. update_time_item_url = add_url_info(update_time_item_url, update_time_item["extra_info"])
  815. value = f"{rtmp_url}{update_time_item["id"]}" if rtmp_url else update_time_item_url
  816. if config.update_time_position == "top":
  817. content = f"🕘️更新时间,#genre#\n{now},{value}\n\n{content}"
  818. else:
  819. content += f"\n\n🕘️更新时间,#genre#\n{now},{value}"
  820. if rtmp_url:
  821. conn = get_db_connection(constants.rtmp_data_path)
  822. try:
  823. cursor = conn.cursor()
  824. cursor.execute(
  825. "CREATE TABLE IF NOT EXISTS result_data (id TEXT PRIMARY KEY, url TEXT, headers TEXT)"
  826. )
  827. for data_list in result_data.values():
  828. for item in data_list:
  829. cursor.execute(
  830. "INSERT OR REPLACE INTO result_data (id, url, headers) VALUES (?, ?, ?)",
  831. (item["id"], item["url"], json.dumps(item.get("headers", None)))
  832. )
  833. conn.commit()
  834. finally:
  835. return_db_connection(constants.rtmp_data_path, conn)
  836. with open(path, "w", encoding="utf-8") as f:
  837. f.write(content)
  838. convert_to_m3u(path, first_channel_name, data=result_data)
  839. def write_channel_to_file(data, epg=None, ipv6=False, first_channel_name=None):
  840. """
  841. Write channel to file
  842. """
  843. try:
  844. print("Write channel to file...")
  845. output_dir = constants.output_dir
  846. dir_list = [
  847. output_dir,
  848. f"{output_dir}/epg",
  849. f"{output_dir}/ipv4",
  850. f"{output_dir}/ipv6",
  851. f"{output_dir}/data",
  852. f"{output_dir}/log",
  853. ]
  854. for dir_name in dir_list:
  855. os.makedirs(dir_name, exist_ok=True)
  856. if epg:
  857. write_to_xml(epg, constants.epg_result_path)
  858. compress_to_gz(constants.epg_result_path, constants.epg_gz_result_path)
  859. open_empty_category = config.open_empty_category
  860. ipv_type_prefer = list(config.ipv_type_prefer)
  861. if any(pref in ipv_type_prefer for pref in ["自动", "auto"]):
  862. ipv_type_prefer = ["ipv6", "ipv4"] if ipv6 else ["ipv4", "ipv6"]
  863. origin_type_prefer = config.origin_type_prefer
  864. address = get_ip_address()
  865. live_url = f"{address}/live/"
  866. hls_url = f"{address}/hls/"
  867. file_list = [
  868. {"path": config.final_file, "enable_log": True},
  869. {"path": constants.ipv4_result_path, "ipv_type_prefer": ["ipv4"]},
  870. {"path": constants.ipv6_result_path, "ipv_type_prefer": ["ipv6"]}
  871. ]
  872. if config.open_rtmp and not os.getenv("GITHUB_ACTIONS"):
  873. file_list += [
  874. {"path": constants.live_result_path, "live": True},
  875. {
  876. "path": constants.live_ipv4_result_path,
  877. "live": True,
  878. "ipv_type_prefer": ["ipv4"]
  879. },
  880. {
  881. "path": constants.live_ipv6_result_path,
  882. "live": True,
  883. "ipv_type_prefer": ["ipv6"]
  884. },
  885. {"path": constants.hls_result_path, "hls": True},
  886. {
  887. "path": constants.hls_ipv4_result_path,
  888. "hls": True,
  889. "ipv_type_prefer": ["ipv4"]
  890. },
  891. {
  892. "path": constants.hls_ipv6_result_path,
  893. "hls": True,
  894. "ipv_type_prefer": ["ipv6"]
  895. },
  896. ]
  897. for file in file_list:
  898. process_write_content(
  899. path=file["path"],
  900. data=data,
  901. live=file.get("live", False),
  902. hls=file.get("hls", False),
  903. live_url=live_url,
  904. hls_url=hls_url,
  905. open_empty_category=open_empty_category,
  906. ipv_type_prefer=file.get("ipv_type_prefer", ipv_type_prefer),
  907. origin_type_prefer=origin_type_prefer,
  908. first_channel_name=first_channel_name,
  909. enable_print=file.get("enable_log", False),
  910. )
  911. print("✅ Write channel to file success")
  912. except Exception as e:
  913. print(f"❌ Write channel to file failed: {e}")
  914. def get_multicast_fofa_search_org(region, org_type):
  915. """
  916. Get the fofa search organization for multicast
  917. """
  918. org = None
  919. if region == "北京" and org_type == "联通":
  920. org = "China Unicom Beijing Province Network"
  921. elif org_type == "联通":
  922. org = "CHINA UNICOM China169 Backbone"
  923. elif org_type == "电信":
  924. org = "Chinanet"
  925. elif org_type == "移动":
  926. org = "China Mobile communications corporation"
  927. return org
  928. def get_multicast_fofa_search_urls():
  929. """
  930. Get the fofa search urls for multicast
  931. """
  932. rtp_file_names = []
  933. for filename in os.listdir(resource_path("config/rtp")):
  934. if filename.endswith(".txt") and "_" in filename:
  935. filename = filename.replace(".txt", "")
  936. rtp_file_names.append(filename)
  937. region_list = config.multicast_region_list
  938. region_type_list = [
  939. (parts[0], parts[1])
  940. for name in rtp_file_names
  941. if (parts := name.partition("_"))[0] in region_list
  942. or "all" in region_list
  943. or "ALL" in region_list
  944. or "全部" in region_list
  945. ]
  946. search_urls = []
  947. for region, r_type in region_type_list:
  948. search_url = "https://fofa.info/result?qbase64="
  949. search_txt = f'"udpxy" && country="CN" && region="{region}" && org="{get_multicast_fofa_search_org(region, r_type)}"'
  950. bytes_string = search_txt.encode("utf-8")
  951. search_txt = base64.b64encode(bytes_string).decode("utf-8")
  952. search_url += search_txt
  953. search_urls.append((search_url, region, r_type))
  954. return search_urls
  955. def get_channel_data_cache_with_compare(data, new_data):
  956. """
  957. Get channel data with cache compare new data
  958. """
  959. for cate, obj in new_data.items():
  960. for name, url_info in obj.items():
  961. if url_info and cate in data and name in data[cate]:
  962. new_urls = {
  963. info["url"]: info["resolution"]
  964. for info in url_info
  965. }
  966. updated_data = []
  967. for info in data[cate][name]:
  968. url = info["url"]
  969. if url in new_urls:
  970. resolution = new_urls[url]
  971. updated_data.append({
  972. "id": info["id"],
  973. "url": url,
  974. "date": info["date"],
  975. "resolution": resolution,
  976. "origin": info["origin"],
  977. "ipv_type": info["ipv_type"]
  978. })
  979. data[cate][name] = updated_data