Merge branch 'dev'

This commit is contained in:
guorong.zheng 2025-01-08 18:31:17 +08:00
commit e52016dadc
17 changed files with 195 additions and 88 deletions

@ -6,7 +6,6 @@ tkinter_ui
**/*.md
**/*.jpg
**/*.png
version.json
.git
.github
.gitignore

@ -33,7 +33,7 @@ jobs:
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: IPTV
name: IPTV-API
path: dist
- name: Get version from version.json
@ -76,6 +76,6 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: dist/IPTV.exe
asset_name: IPTV.exe
asset_path: dist/IPTV-API.exe
asset_name: IPTV-API.exe
asset_content_type: application/octet-stream

@ -1,5 +1,53 @@
# 更新日志Changelog
## v1.5.9
### 2025/1/8
- ❤️ 2025年第一次更新祝大家新年快乐万事如意
- ✨ 公众号详细教程文章已发布,欢迎关注`Govin`公众号获取
- ✨ 新增支持`rtmp`协议接口(#780
- ✨ 新增支持修改更新时间位置(`update_time_position`#755
- ✨ 新增支持修改时区(`time_zone`#759
- ✨ 更新组播源与酒店源离线数据,增加`广东移动组播RTP`#773
- ✨ 更新Github CDN代理地址#796
- ✨ GUI使用Github工作流基于源码自动构建并发布唯一下载途径是[Release](https://github.com/Guovin/iptv-api/releases)
,若安全软件有误报,请添加信任
- ✨ 增加版本信息打印输出
- ✨ 更新部分教程文档图片
- 🐛 修复m3u更新时间logo显示问题#794
- 🐛 修复测速阶段出现`cookie illegal key`问题(#728,#787
- 🐛 修复白名单接口排序与接口信息命名问题(#765
- 🐛 修复组播源更新结果异常问题
- 🐛 修复写入结果目录为空问题
- 🪄 调整接口状态码判断,只处理`200`状态码(#779
- 🪄 调整默认不显示接口信息,兼容更多播放器
<details>
<summary>English</summary>
- ❤️ First update of 2025, wishing everyone a Happy New Year and all the best
- ✨ Detailed tutorial articles have been published on the `Govin` public account, welcome to follow for more information
- ✨ Added support for `rtmp` protocol interface (#780)
- ✨ Added support for modifying update time position (`update_time_position`) (#755)
- ✨ Added support for modifying time zone (`time_zone`) (#759)
- ✨ Updated offline data for multicast sources and hotel sources, added `Guangdong Mobile Multicast RTP` (#773)
- ✨ Updated GitHub CDN proxy address (#796)
- ✨ GUI is automatically built and released based on the source code using GitHub workflows, the only download method
is [Release](https://github.com/Guovin/iptv-api/releases). If there are false positives from security software, please
add it to the trust list
- ✨ Added version information print output
- ✨ Updated some tutorial document images
- 🐛 Fixed m3u update time logo display issue (#794)
- 🐛 Fixed `cookie illegal key` issue during speed test phase (#728, #787)
- 🐛 Fixed whitelist interface sorting and interface information naming issue (#765)
- 🐛 Fixed abnormal results issue for multicast source updates
- 🐛 Fixed empty result directory issue
- 🪄 Adjusted interface status code judgment to only process `200` status code (#779)
- 🪄 Adjusted to hide interface information by default, compatible with more players
</details>
## v1.5.8
### 2024/12/30

@ -25,6 +25,7 @@ flask = "*"
opencc-python-reimplemented = "*"
pillow = "*"
m3u8 = "*"
pytz = "*"
[packages]
requests = "*"
@ -37,6 +38,7 @@ opencc-python-reimplemented = "*"
gunicorn = "*"
pillow = "*"
m3u8 = "*"
pytz = "*"
[requires]
python_version = "3.13"

18
Pipfile.lock generated

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "7ccc0e57a8b15b60d41f3ca958d5da9a7391392e60967d053377d60a93998b1b"
"sha256": "919159f0f1a8d07db897c47ac59358ec36f164cf6698d1b21fdeef94a72340dc"
},
"pipfile-spec": 6,
"requires": {
@ -780,6 +780,14 @@
"markers": "python_version >= '3.9'",
"version": "==0.2.1"
},
"pytz": {
"hashes": [
"sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a",
"sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"
],
"index": "aliyun",
"version": "==2024.2"
},
"requests": {
"hashes": [
"sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760",
@ -1815,6 +1823,14 @@
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.7.1"
},
"pytz": {
"hashes": [
"sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a",
"sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"
],
"index": "aliyun",
"version": "==2024.2"
},
"pywin32-ctypes": {
"hashes": [
"sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8",

@ -164,7 +164,7 @@ https://cdn.jsdelivr.net/gh/Guovin/iptv-api@gd/source.json
| open_subscribe | 开启订阅源功能 | False |
| open_update | 开启更新,用于控制是否更新接口,若关闭则所有工作模式(获取接口和测速)均停止 | True |
| open_update_time | 开启显示更新时间 | True |
| open_url_info | 开启显示接口说明信息,用于控制是否显示接口来源、分辨率、协议类型等信息,为$符号后的内容播放软件使用该信息对接口进行描述若部分播放器如PotPlayer不支持解析导致无法播放可关闭 | True |
| open_url_info | 开启显示接口说明信息,用于控制是否显示接口来源、分辨率、协议类型等信息,为$符号后的内容播放软件使用该信息对接口进行描述若部分播放器如PotPlayer不支持解析导致无法播放可关闭 | False |
| open_use_cache | 开启使用本地缓存数据,适用于查询请求失败场景(仅针对酒店源与组播源) | True |
| open_use_old_result | 开启使用历史更新结果(包含模板与结果文件的接口),合并至本次更新中 | True |
| app_port | 页面服务端口,用于控制页面服务的端口号 | 8000 |
@ -190,7 +190,9 @@ https://cdn.jsdelivr.net/gh/Guovin/iptv-api@gd/source.json
| sort_timeout | 单个接口测速超时时长,单位秒(s);数值越大测速所属时间越长,能提高获取接口数量,但质量会有所下降;数值越小测速所需时间越短,能获取低延时的接口,质量较好;调整此值能优化更新时间 | 10 |
| source_file | 模板文件路径 | config/demo.txt |
| subscribe_num | 结果中偏好的订阅源接口数量 | 10 |
| time_zone | 时区可用于控制更新时间显示的时区可选值Asia/Shanghai 或其它时区编码 | Asia/Shanghai |
| urls_limit | 单个频道接口数量 | 10 |
| update_time_position | 更新时间显示位置,需要开启 open_update_time 才能生效可选值top、bottomtop: 显示于结果顶部bottom: 显示于结果底部 | top |
## 快速上手

@ -164,7 +164,7 @@ https://cdn.jsdelivr.net/gh/Guovin/iptv-api@gd/source.json
| open_subscribe | Enable subscription source feature | True |
| open_update | Enable updates, if disabled then only the result page service is run | True |
| open_update_time | Enable show update time | True |
| open_url_info | Enable to display interface description information, used to control whether to display interface source, resolution, protocol type and other information, the content after the $ symbol, the playback software uses this information to describe the interface, if some players (such as PotPlayer) do not support parsing and cannot play, you can turn it off | True |
| open_url_info | Enable to display interface description information, used to control whether to display interface source, resolution, protocol type and other information, the content after the $ symbol, the playback software uses this information to describe the interface, if some players (such as PotPlayer) do not support parsing and cannot play, you can turn it off | False |
| open_use_cache | Enable the use of local cache data, applicable to the query request failure scenario (only for hotel sources and multicast sources) | True |
| open_use_old_result | Enable the use of historical update results (including the interface for template and result files) and merge them into the current update | True |
| app_port | Page service port, used to control the port number of the page service | 8000 |
@ -190,7 +190,9 @@ https://cdn.jsdelivr.net/gh/Guovin/iptv-api@gd/source.json
| sort_timeout | The timeout duration for speed testing of a single interface, in seconds (s). A larger value means a longer testing period, which can increase the number of interfaces obtained but may decrease their quality. A smaller value means a shorter testing time, which can obtain low-latency interfaces with better quality. Adjusting this value can optimize the update time. | 10 |
| source_file | Template file path | config/demo.txt |
| subscribe_num | The number of preferred subscribe source interfaces in the results | 10 |
| time_zone | Time zone, can be used to control the time zone displayed by the update time, optional values: Asia/Shanghai or other time zone codes | Asia/Shanghai |
| urls_limit | Number of interfaces per channel | 10 |
| update_time_position | Update time display position, need to enable open_update_time to take effect, optional values: top, bottom, top: display at the top of the result, bottom: display at the bottom of the result | top |
## Quick Start

@ -42,7 +42,7 @@ open_update = True
# 开启显示更新时间; 可选值: True, False | Enable display update time; Optional values: True, False
open_update_time = True
# 开启显示接口说明信息,用于控制是否显示接口来源、分辨率、协议类型等信息,为$符号后的内容播放软件使用该信息对接口进行描述若部分播放器如PotPlayer不支持解析导致无法播放可关闭; 可选值: True, False | Enable to display interface description information, used to control whether to display interface source, resolution, protocol type and other information, the content after the $ symbol, the playback software uses this information to describe the interface, if some players (such as PotPlayer) do not support parsing and cannot play, you can turn it off; Optional values: True, False
open_url_info = True
open_url_info = False
# 开启使用本地缓存数据,适用于查询请求失败场景(仅针对酒店源与组播源); 可选值: True, False | Enable to use local cached data, suitable for query request failure scenarios (only for hotel source and multicast source); Optional values: True, False
open_use_cache = True
# 开启使用历史更新结果(包含模板与结果文件的接口),合并至本次更新中; 可选值: True, False | Enable to use historical update results (including interfaces of templates and result files), merged into this update; Optional values: True, False
@ -93,5 +93,9 @@ sort_timeout = 10
source_file = config/demo.txt
# 结果中偏好的订阅源接口数量 | Preferred number of subscription source interfaces in the result
subscribe_num = 10
# 时区可用于控制更新时间显示的时区可选值Asia/Shanghai 或其它时区编码 | Time zone, can be used to control the time zone displayed by the update time, optional values: Asia/Shanghai or other time zone codes
time_zone = Asia/Shanghai
# 单个频道接口数量 | Number of interfaces per channel
urls_limit = 10
urls_limit = 10
# 更新时间显示位置,需要开启 open_update_time 才能生效可选值top、bottomtop: 显示于结果顶部bottom: 显示于结果底部 | Update time display position, need to enable open_update_time to take effect, optional values: top, bottom, top: display at the top of the result, bottom: display at the bottom of the result
update_time_position = top

@ -20,7 +20,7 @@
| open_subscribe | 开启订阅源功能 | False |
| open_update | 开启更新,用于控制是否更新接口,若关闭则所有工作模式(获取接口和测速)均停止 | True |
| open_update_time | 开启显示更新时间 | True |
| open_url_info | 开启显示接口说明信息,用于控制是否显示接口来源、分辨率、协议类型等信息,为$符号后的内容播放软件使用该信息对接口进行描述若部分播放器如PotPlayer不支持解析导致无法播放可关闭 | True |
| open_url_info | 开启显示接口说明信息,用于控制是否显示接口来源、分辨率、协议类型等信息,为$符号后的内容播放软件使用该信息对接口进行描述若部分播放器如PotPlayer不支持解析导致无法播放可关闭 | False |
| open_use_cache | 开启使用本地缓存数据,适用于查询请求失败场景(仅针对酒店源与组播源) | True |
| open_use_old_result | 开启使用历史更新结果(包含模板与结果文件的接口),合并至本次更新中 | True |
| app_port | 页面服务端口,用于控制页面服务的端口号 | 8000 |
@ -46,4 +46,6 @@
| sort_timeout | 单个接口测速超时时长,单位秒(s);数值越大测速所属时间越长,能提高获取接口数量,但质量会有所下降;数值越小测速所需时间越短,能获取低延时的接口,质量较好;调整此值能优化更新时间 | 10 |
| source_file | 模板文件路径 | config/demo.txt |
| subscribe_num | 结果中偏好的订阅源接口数量 | 10 |
| urls_limit | 单个频道接口数量 | 10 |
| time_zone | 时区可用于控制更新时间显示的时区可选值Asia/Shanghai 或其它时区编码 | Asia/Shanghai |
| urls_limit | 单个频道接口数量 | 10 |
| update_time_position | 更新时间显示位置,需要开启 open_update_time 才能生效可选值top、bottomtop: 显示于结果顶部bottom: 显示于结果底部 | top |

@ -20,7 +20,7 @@
| open_subscribe | Enable subscription source feature | True |
| open_update | Enable updates, if disabled then only the result page service is run | True |
| open_update_time | Enable show update time | True |
| open_url_info | Enable to display interface description information, used to control whether to display interface source, resolution, protocol type and other information, the content after the $ symbol, the playback software uses this information to describe the interface, if some players (such as PotPlayer) do not support parsing and cannot play, you can turn it off | True |
| open_url_info | Enable to display interface description information, used to control whether to display interface source, resolution, protocol type and other information, the content after the $ symbol, the playback software uses this information to describe the interface, if some players (such as PotPlayer) do not support parsing and cannot play, you can turn it off | False |
| open_use_cache | Enable the use of local cache data, applicable to the query request failure scenario (only for hotel sources and multicast sources) | True |
| open_use_old_result | Enable the use of historical update results (including the interface for template and result files) and merge them into the current update | True |
| app_port | Page service port, used to control the port number of the page service | 8000 |
@ -46,4 +46,6 @@
| sort_timeout | The timeout duration for speed testing of a single interface, in seconds (s). A larger value means a longer testing period, which can increase the number of interfaces obtained but may decrease their quality. A smaller value means a shorter testing time, which can obtain low-latency interfaces with better quality. Adjusting this value can optimize the update time. | 10 |
| source_file | Template file path | config/demo.txt |
| subscribe_num | The number of preferred subscribe source interfaces in the results | 10 |
| urls_limit | Number of interfaces per channel | 10 |
| time_zone | Time zone, can be used to control the time zone displayed by the update time, optional values: Asia/Shanghai or other time zone codes | Asia/Shanghai |
| urls_limit | Number of interfaces per channel | 10 |
| update_time_position | Update time display position, need to enable open_update_time to take effect, optional values: top, bottom, top: display at the top of the result, bottom: display at the bottom of the result | top |

10
main.py

@ -30,7 +30,8 @@ from utils.tools import (
format_interval,
check_ipv6_support,
resource_path,
get_urls_from_file
get_urls_from_file,
get_version_info
)
@ -116,6 +117,9 @@ class UpdateSource:
for channel_obj in self.channel_items.values()
for name in channel_obj.keys()
]
if not channel_names:
print(f"❌ No channel names found! Please check the {config.source_file}!")
return
await self.visit_page(channel_names)
self.tasks = []
append_total_data(
@ -169,7 +173,7 @@ class UpdateSource:
"wb",
) as file:
pickle.dump(channel_data_cache, file)
convert_to_m3u()
convert_to_m3u(channel_names[0])
print(
f"🥳 Update completed! Total time spent: {format_interval(time() - main_start_time)}. Please check the {user_final_file} file!"
)
@ -209,6 +213,8 @@ class UpdateSource:
if __name__ == "__main__":
info = get_version_info()
print(f" {info['name']} Version: {info['version']}")
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
update_source = UpdateSource()

@ -292,6 +292,19 @@ class DefaultUI:
)
self.open_update_time_checkbutton.pack(side=tk.LEFT, padx=4, pady=8)
self.update_time_position_label = tk.Label(
frame_default_open_update_info_column1, text="位置:", width=3
)
self.update_time_position_label.pack(side=tk.LEFT, padx=4, pady=8)
self.update_time_position_combo = ttk.Combobox(frame_default_open_update_info_column1, width=5)
self.update_time_position_combo.pack(side=tk.LEFT, padx=4, pady=8)
self.update_time_position_combo["values"] = ("顶部", "底部")
if config.update_time_position == "bottom":
self.update_time_position_combo.current(1)
else:
self.update_time_position_combo.current(0)
self.update_time_position_combo.bind("<<ComboboxSelected>>", self.update_update_time_position)
self.open_url_info_label = tk.Label(
frame_default_open_update_info_column2, text="显示接口信息:", width=12
)
@ -345,6 +358,18 @@ class DefaultUI:
)
self.ipv6_support_checkbutton.pack(side=tk.LEFT, padx=4, pady=8)
frame_time_zone = tk.Frame(root)
frame_time_zone.pack(fill=tk.X)
self.time_zone_label = tk.Label(
frame_time_zone, text="时区:", width=12
)
self.time_zone_label.pack(side=tk.LEFT, padx=4, pady=8)
self.time_zone_entry = tk.Entry(frame_time_zone, width=18)
self.time_zone_entry.pack(side=tk.LEFT, padx=4, pady=8)
self.time_zone_entry.insert(0, config.time_zone)
self.time_zone_entry.bind("<KeyRelease>", self.update_time_zone)
frame_default_url_keywords = tk.Frame(root)
frame_default_url_keywords.pack(fill=tk.X)
frame_default_url_keywords_column1 = tk.Frame(frame_default_url_keywords)
@ -431,6 +456,9 @@ class DefaultUI:
def update_urls_limit(self, event):
config.set("Settings", "urls_limit", self.urls_limit_entry.get())
def update_time_zone(self, event):
config.set("Settings", "time_zone", self.time_zone_entry.get())
def update_open_update_time(self):
config.set("Settings", "open_update_time", str(self.open_update_time_var.get()))
@ -450,6 +478,10 @@ class DefaultUI:
def update_ipv_type(self, event):
config.set("Settings", "ipv_type", self.ipv_type_combo.get())
def update_update_time_position(self, event):
config.set("Settings", "update_time_position",
'bottom' if self.update_time_position_combo.get() == '底部' else 'top')
def edit_whitelist_file(self):
path = resource_path(constants.whitelist_path)
if os.path.exists(path):
@ -473,11 +505,13 @@ class DefaultUI:
"request_timeout_entry",
"source_file_entry",
"source_file_button",
"time_zone_entry",
"final_file_entry",
"final_file_button",
"open_keep_all_checkbutton",
"open_m3u_result_checkbutton",
"urls_limit_entry",
"update_time_position_combo",
"open_update_time_checkbutton",
"open_url_info_checkbutton",
"open_empty_category_checkbutton",

@ -6,7 +6,7 @@ import tkinter as tk
from tkinter import messagebox
from PIL import Image, ImageTk
from utils.config import config
from utils.tools import resource_path
from utils.tools import resource_path, get_version_info
from main import UpdateSource
import asyncio
import threading
@ -19,14 +19,12 @@ from multicast import MulticastUI
from hotel import HotelUI
from subscribe import SubscribeUI
from online_search import OnlineSearchUI
import json
from utils.speed import check_ffmpeg_installed_status
class TkinterUI:
def __init__(self, root):
with open(resource_path("version.json"), "r", encoding="utf-8") as f:
info = json.load(f)
info = get_version_info()
self.root = root
self.root.title(info.get("name", ""))
self.version = info.get("version", "")
@ -46,46 +44,6 @@ class TkinterUI:
webbrowser.open_new_tab(self.result_url)
def save_config(self):
config_values = {
"open_driver": self.default_ui.open_driver_var.get(),
"open_filter_resolution": self.speed_ui.open_filter_resolution_var.get(),
"open_hotel": self.hotel_ui.open_hotel_var.get(),
"open_hotel_foodie": self.hotel_ui.open_hotel_foodie_var.get(),
"open_hotel_fofa": self.hotel_ui.open_hotel_fofa_var.get(),
"open_keep_all": self.default_ui.open_keep_all_var.get(),
"open_multicast": self.multicast_ui.open_multicast_var.get(),
"open_multicast_foodie": self.multicast_ui.open_multicast_foodie_var.get(),
"open_multicast_fofa": self.multicast_ui.open_multicast_fofa_var.get(),
"open_online_search": self.online_search_ui.open_online_search_var.get(),
"open_proxy": self.default_ui.open_proxy_var.get(),
"open_request": self.default_ui.open_request_var.get(),
"open_service": self.default_ui.open_service_var.get(),
"open_sort": self.speed_ui.open_sort_var.get(),
"open_subscribe": self.subscribe_ui.open_subscribe_var.get(),
"open_supply": self.prefer_ui.open_supply_var.get(),
"open_update": self.default_ui.open_update_var.get(),
"open_update_time": self.default_ui.open_update_time_var.get(),
"open_url_info": self.default_ui.open_url_info_var.get(),
"open_use_cache": self.default_ui.open_use_cache_var.get(),
"open_use_old_result": self.default_ui.open_use_old_result_var.get(),
"final_file": self.default_ui.final_file_entry.get(),
"hotel_region_list": self.hotel_ui.region_list_combo.get(),
"hotel_page_num": self.hotel_ui.page_num_entry.get(),
"ipv_type": self.default_ui.ipv_type_combo.get(),
"ipv6_support": self.default_ui.ipv6_support_var.get(),
"min_resolution": self.speed_ui.min_resolution_entry.get(),
"multicast_region_list": self.multicast_ui.region_list_combo.get(),
"multicast_page_num": self.multicast_ui.page_num_entry.get(),
"online_search_page_num": self.online_search_ui.page_num_entry.get(),
"recent_days": self.online_search_ui.recent_days_entry.get(),
"request_timeout": self.default_ui.request_timeout_entry.get(),
"sort_timeout": self.speed_ui.sort_timeout_entry.get(),
"source_file": self.default_ui.source_file_entry.get(),
"urls_limit": self.default_ui.urls_limit_entry.get(),
}
for key, value in config_values.items():
config.set("Settings", key, str(value))
config.save()
messagebox.showinfo("提示", "保存成功")
@ -274,7 +232,7 @@ def get_root_location(root):
screen_width = root.winfo_screenwidth()
screen_height = root.winfo_screenheight()
width = 500
height = 600
height = 650
x = (screen_width / 2) - (width / 2)
y = (screen_height / 2) - (height / 2)
return (width, height, x, y)

@ -1,7 +1,6 @@
import asyncio
import base64
import copy
import datetime
import os
import pickle
import re
@ -27,10 +26,10 @@ from utils.tools import (
add_url_info,
remove_cache_info,
resource_path,
write_content_into_txt,
get_urls_from_file,
get_name_urls_from_file,
get_logger,
get_datetime_now
)
@ -634,23 +633,12 @@ def write_channel_to_file(data, ipv6=False, callback=None):
if any(pref in ipv_type_prefer for pref in ["自动", "auto"]) or not ipv_type_prefer:
ipv_type_prefer = ["ipv6", "ipv4"] if ipv6 else ["ipv4", "ipv6"]
origin_type_prefer = config.origin_type_prefer
if config.open_update_time:
now = datetime.datetime.now()
if os.environ.get("GITHUB_ACTIONS"):
now += datetime.timedelta(hours=8)
update_time = now.strftime("%Y-%m-%d %H:%M:%S")
update_time_url = next(
(get_total_urls(info_list, ipv_type_prefer, origin_type_prefer)[0]
for channel_obj in data.values()
for info_list in channel_obj.values() if info_list),
"url"
)
write_content_into_txt(f"🕘️更新时间,#genre#", path, newline=False)
write_content_into_txt(f"{update_time},{update_time_url}", path)
write_content_into_txt("", path)
first_cate = True
content = ""
for cate, channel_obj in data.items():
print(f"\n{cate}:", end=" ")
write_content_into_txt(f"{cate},#genre#", path)
content += f"{'\n\n' if not first_cate else ''}{cate},#genre#"
first_cate = False
channel_obj_keys = channel_obj.keys()
names_len = len(list(channel_obj_keys))
for i, name in enumerate(channel_obj_keys):
@ -663,17 +651,31 @@ def write_channel_to_file(data, ipv6=False, callback=None):
no_result_name.append(name)
continue
for url in channel_urls:
write_content_into_txt(f"{name},{url}", path, callback=callback)
content += f"\n{name},{url}"
if callback:
callback()
print()
write_content_into_txt("", path)
if open_empty_category and no_result_name:
print("\n🈳 No result channel name:")
write_content_into_txt("🈳无结果频道,#genre#", path)
content += "\n\n🈳无结果频道,#genre#"
for i, name in enumerate(no_result_name):
end_char = ", " if i < len(no_result_name) - 1 else ""
print(name, end=end_char)
write_content_into_txt(f"{name},url", path)
content += f"\n{name},url"
print()
if config.open_update_time:
update_time_url = next(
(get_total_urls(info_list, ipv_type_prefer, origin_type_prefer)[0]
for channel_obj in data.values()
for info_list in channel_obj.values() if info_list),
"url"
)
if config.update_time_position == "top":
content = f"🕘️更新时间,#genre#\n{get_datetime_now()},{update_time_url}\n\n{content}"
else:
content += f"\n\n🕘️更新时间,#genre#\n{get_datetime_now()},{update_time_url}"
with open(path, "w", encoding="utf-8") as f:
f.write(content)
except Exception as e:
print(f"❌ Write channel to file failed: {e}")

@ -307,6 +307,14 @@ class ConfigManager:
def open_supply(self):
return self.config.getboolean("Settings", "open_supply", fallback=True)
@property
def update_time_position(self):
return self.config.get("Settings", "update_time_position", fallback="top")
@property
def time_zone(self):
return self.config.get("Settings", "time_zone", fallback="Asia/Shanghai")
def load(self):
"""
Load the config

@ -1,5 +1,6 @@
import datetime
import ipaddress
import json
import logging
import os
import re
@ -11,6 +12,7 @@ from collections import defaultdict
from logging.handlers import RotatingFileHandler
from time import time
import pytz
import requests
from bs4 import BeautifulSoup
from flask import send_file, make_response
@ -348,7 +350,7 @@ def get_ip_address():
return f"http://{ip}:{config.app_port}"
def convert_to_m3u():
def convert_to_m3u(first_channel_name=None):
"""
Convert result txt to m3u format
"""
@ -373,7 +375,7 @@ def convert_to_m3u():
r"(CCTV|CETV)-(\d+)(\+.*)?",
lambda m: f"{m.group(1)}{m.group(2)}"
+ ("+" if m.group(3) else ""),
original_channel_name,
first_channel_name if current_group == "🕘️更新时间" else original_channel_name,
)
m3u_output += f'#EXTINF:-1 tvg-name="{processed_channel_name}" tvg-logo="https://live.fanmingming.cn/tv/{processed_channel_name}.png"'
if current_group:
@ -499,16 +501,19 @@ def resource_path(relative_path, persistent=False):
return total_path
def write_content_into_txt(content, path=None, newline=True, callback=None):
def write_content_into_txt(content, path=None, position=None, callback=None):
"""
Write content into txt file
"""
if not path:
return
with open(path, "a", encoding="utf-8") as f:
if newline:
f.write(f"\n{content}")
mode = "r+" if position == "top" else "a"
with open(path, mode, encoding="utf-8") as f:
if position == "top":
existing_content = f.read()
f.seek(0, 0)
f.write(f"{content}\n{existing_content}")
else:
f.write(content)
@ -579,3 +584,20 @@ def get_name_urls_from_file(path: str) -> dict[str, list]:
if url not in name_urls[name]:
name_urls[name].append(url)
return name_urls
def get_datetime_now():
"""
Get the datetime now
"""
now = datetime.datetime.now()
time_zone = pytz.timezone(config.time_zone)
return now.astimezone(time_zone).strftime("%Y-%m-%d %H:%M:%S")
def get_version_info():
"""
Get the version info
"""
with open(resource_path("version.json"), "r", encoding="utf-8") as f:
return json.load(f)

@ -1,4 +1,4 @@
{
"version": "1.5.8",
"version": "1.5.9",
"name": "IPTV-API"
}