好友
阅读权限10
听众
最后登录1970-1-1
|
本帖最后由 royswift 于 2026-4-1 10:56 编辑
M3U8 VIDEO SNIFFER
面向 Windows 桌面环境的流媒体资源嗅探、解析与下载工具。
以 Python + PyQt6 构建的桌面应用,核心目标是把“打开网页、播放资源、识别媒体地址、选择下载引擎、查看进度与结果”整合到同一个工作流中。

不会代码,纯ai写的。我只负责口语化的制定需求。所以不算原创。
小白没经验走了好多弯路,所以一个下载器鼓捣了四五个版本了。
之前只会上传代码,还得配置各种环境,用起来挺麻烦的。
这版本第一次制作了个exe的Releases包。
exe安装包可以选中安装运行环境(不含chrome),省心。
更详细的看程序右上角的 “快速手册”,非常详细。

M3U8 VIDEO SNIFFER(github)
https://github.com/royswift2007/m3u8-video-sniffer

首次运行前必看:运行环境与 Chrome 重要说明
运行环境总览(请先确认)
> 重要:当前程序的内置浏览器实际依赖系统已安装的 Google Chrome。
> 不是只安装 playwright install chromium 就能替代。
> 如果没有安装 Chrome,浏览器工作台、网页嗅探、Cookie/登录态复用、部分站点下载能力都会明显受影响。
| 类别 |
名称 |
要求/版本 |
是否必需 |
说明 |
| 操作系统 |
Windows |
Windows 10/11 64 位 |
必需 |
当前项目按 Windows 桌面环境设计。 |
| Python 运行时 |
Python |
3.9 或更高版本 |
必需 |
用于运行 main.py / mvs.pyw。 |
| Python 包管理 |
pip |
可正常使用 |
必需 |
需要执行 pip install -r requirements.txt 安装依赖。 |
| Python 依赖 |
requirements.txt |
需安装完成 |
必需 |
包含 PyQt6 / PyQt6-WebEngine / plyer / requests / playwright。 |
| 浏览器环境 |
Google Chrome |
系统已安装且可正常启动 |
强制要求(内置浏览器场景) |
当前程序依赖系统 Chrome,不是 playwright install chromium。 |
| 下载引擎 |
bin/yt-dlp.exe |
文件存在且可执行 |
必需 |
页面型站点与大量通用站点下载依赖它。 |
| 下载引擎 |
bin/N_m3u8DL-RE.exe |
文件存在且可执行 |
必需 |
m3u8 / mpd / HLS / DASH 下载核心引擎。 |
| 下载引擎 |
bin/ffmpeg.exe |
文件存在且可执行 |
必需 |
音视频合并、转封装、部分后处理依赖它。 |
| 下载引擎 |
bin/aria2c.exe |
文件存在且可执行 |
建议安装 |
直链资源、多连接下载、磁力场景更依赖它。 |
| 下载引擎 |
bin/streamlink.exe |
文件存在且可执行 |
建议安装 |
直播流 / 直播回放任务更依赖它。 |
| 辅助工具 |
bin/deno.exe |
文件存在且可执行 |
可选 |
当前主流程不是强依赖,但建议保留。 |
| 网络环境 |
GitHub / 常见资源站点可访问 |
建议稳定联网 |
建议 |
首次安装依赖、下载工具、站点解析依赖网络连通性。 |
| 磁盘空间 |
本地可用空间 |
至少 500MB,建议 2GB 以上 |
必需 |
工具本体、缓存、临时文件、合并中间文件都会占用空间。 |
| 浏览器扩展 |
CatCatch(猫爪) |
按需安装 |
可选 |
仅在你希望从 Chrome/Edge 一键发送资源到程序时需要。 |
未安装 Chrome 会导致:
浏览器工作台无法正常启动
网页内自动嗅探失效
浏览器 Cookie / 登录态能力失效
部分站点真实媒体地址无法捕获
整体下载能力明显降级
结论:Google Chrome 不是可有可无的附加项,而是影响内置浏览器、嗅探成功率、登录态复用和整体使用体验的关键前提。





1. 应用启动与外部 URL 注入,来自 main.py
[Python] 纯文本查看 复制代码 def main():
"""应用主入口:负责启动 Qt 程序,并接收外部传入的下载资源。"""
args = parse_args()
_merge_chromium_flags() # 给 Qt WebEngine 补充 Chromium 参数,降低被站点识别为自动化环境的概率
app = QApplication(sys.argv)
app.setApplicationName("M3U8 Video Sniffer")
app.setOrganizationName("M3U8VideoSniffer")
window = MainWindow() # 创建主窗口,内部会初始化浏览器、嗅探器、下载管理器等组件
window.show()
if args.url:
# 如果程序是被协议调用或命令行带参启动,会把外部资源直接注入到资源面板
headers = {}
if args.headers:
try:
headers = json.loads(args.headers) # 把外部传入的请求头 JSON 解析出来
except json.JSONDecodeError:
logger.warning(f"[CLI] 无法解析 headers: {args.headers}")
def add_external_resource():
from core.engine_selector import EngineSelector
from core.task_model import M3U8Resource
resource = M3U8Resource(
url=args.url,
headers=headers,
page_url=args.url,
title=args.filename or "External Download", # 若外部未给文件名,则使用默认标题
)
selector = EngineSelector(window.engines)
_, engine_name = selector.select(args.url, None) # 根据 URL 自动选择合适下载引擎
window.resource_panel.add_resource(resource, engine_name) # 加入资源列表
window.main_tabs.setCurrentIndex(1) # 自动切到“资源检测”标签页,方便用户确认下载
QTimer.singleShot(500, add_external_resource) # 延迟一点执行,确保主窗口组件已准备完毕
sys.exit(app.exec()) # 进入 Qt 事件循环
2. 媒体资源嗅探、去重与上下文合并,来自 core/m3u8_sniffer.py
[Python] 纯文本查看 复制代码 def add_resource(self, url: str, headers: dict, page_url: str, page_title: str = "") -> Optional[M3U8Resource]:
"""添加嗅探到的资源,并对重复 URL 做上下文合并。"""
headers = headers or {}
url_lower = (url or "").lower()
is_m3u8 = ".m3u8" in url_lower
candidate_score = 0
# 只有 m3u8 资源才做更细的请求头补全和候选评分
if is_m3u8:
headers = self._normalize_m3u8_headers(headers, page_url) # 统一 header 键名,并补 referer / UA
if self._features.get("sniffer_rules_enabled", True):
headers = self._apply_site_rules(url, page_url, headers) # 按站点规则补认证头
candidate_score = self._score_m3u8_candidate(url, headers, page_url) # 给资源打分,便于后续挑选更优链接
# 若 URL 已见过,则不重复新增,而是尝试把新的 page_title / headers 合并回旧资源
if self._features.get("sniffer_dedup_enabled", True) and url in self._seen_urls:
existing = self._find_resource_by_url(url)
if existing:
merged = self._merge_resource_context(
existing,
headers,
page_url,
page_title,
candidate_score,
)
logger.debug("资源已存在,合并上下文" if merged else "资源已存在,跳过", event="sniffer_dedup", url=url, merged=merged)
return existing
return None
# 首次发现资源时,创建资源对象并放入列表
resource = M3U8Resource(
url=url,
headers=headers,
page_url=page_url,
page_title=page_title,
candidate_score=candidate_score,
)
self.resources.append(resource)
self._seen_urls.add(url)
notify_resource_found(resource.title) # 给用户发资源发现通知
if self.on_resource_found:
self.on_resource_found(resource) # 回调 UI,让资源面板立即刷新
return resource
3. 下载任务入队与自动引擎选择,来自 core/download_manager.py
[Python] 纯文本查看 复制代码 def add_task(self, task: DownloadTask, user_engine_preference: str = None):
"""把下载任务加入队列,并为它确定实际执行引擎。"""
with self._lock:
if task in self.active_tasks:
logger.info(f"任务正在执行中,跳过重复入队: {task.filename}")
return
self._remove_task_from_state_lists(task) # 如果任务曾失败/暂停/完成,重新入队前先清理旧状态
if self._is_task_queued(task):
logger.info(f"任务已在队列中,跳过重复入队: {task.filename}")
return
# 依据 URL 和用户偏好自动挑选合适的下载引擎
engine, engine_name = self.selector.select(task.url, user_engine_preference)
self._reset_task_runtime(task) # 重置进度、错误信息、进程句柄等运行时字段
task.engine = engine_name
task.status = "waiting"
user_specified = user_engine_preference is not None
self.task_queue.put((task, engine, user_specified)) # 压入线程安全队列,等待工作线程消费
logger.info(
f"任务已加入队列: {task.filename} (引擎: {engine_name}, 用户指定: {user_specified})"
)
if self.on_task_update:
self.on_task_update(task) # 通知 UI 更新任务状态
4. 智能引擎选择策略,来自 core/engine_selector.py
[Python] 纯文本查看 复制代码 def select(self, url: str, user_preference: str = None) -> tuple[BaseEngine, str]:
"""根据用户指定或自动策略,选出最适合当前 URL 的下载引擎。"""
# 1. 如果用户在界面上手动指定了引擎,优先尊重用户选择
if user_preference and user_preference in self._engine_map:
preferred_engine = self._engine_map[user_preference]
if preferred_engine.can_handle(url):
logger.info(f"使用用户指定的引擎: {user_preference}")
return preferred_engine, user_preference
else:
logger.warning(f"{user_preference} 无法处理该 URL,自动选择引擎...")
# 2. 否则按预设优先级筛选:N_m3u8DL-RE -> Streamlink -> Aria2 -> yt-dlp
candidates = self.get_candidates(url)
if not candidates:
raise RuntimeError("无可用下载引擎,请检查引擎配置或二进制文件")
engine, engine_name = candidates[0]
logger.info(f"自动选择引擎: {engine_name}")
return engine, engine_name
5. yt-dlp 下载回退逻辑,来自 engines/ytdlp_engine.py
[Python] 纯文本查看 复制代码 def download(self, task: DownloadTask, progress_callback) -> bool:
"""执行 yt-dlp 下载;优先使用站点 cookies,必要时再回退到浏览器 cookies。"""
import os
cookies_file = self.get_cookies_file_for_url(task.url) # 根据站点域名推断 cookies 文件位置
has_cookies_file = cookies_file and os.path.exists(cookies_file)
# 第一轮:优先使用用户手动导出的 cookies 文件
if has_cookies_file:
task.headers['_cookie_file'] = cookies_file
logger.info(f"[yt-dlp] 使用手动导出的 cookies: {cookies_file}")
success, need_login = self._do_download(
task,
progress_callback,
use_browser_cookies=None,
allow_insecure_tls=False
)
if success:
return True
# 如果失败原因疑似“需要登录”,则进入补救逻辑
if need_login:
if has_cookies_file:
logger.warning(f"[yt-dlp] cookies 可能已过期,请重新导出 {os.path.basename(cookies_file)}")
else:
logger.warning(f"[yt-dlp] 需要登录但未找到 cookies 文件!")
logger.info(f"[yt-dlp] 尝试使用 Firefox cookies 作为备用...")
task.headers.pop('_cookie_file', None) # 避免继续使用失效 cookies
# 第二轮:退回到浏览器 cookies 抓取方式,提高登录站点下载成功率
success, _ = self._do_download(
task,
progress_callback,
use_browser_cookies='firefox',
allow_insecure_tls=False
)
return success
return False
6. 外部协议解析与投递,来自 protocol_handler.pyw
[Python] 纯文本查看 复制代码 def parse_m3u8dl_url(raw_url: str) -> dict:
"""解析 m3u8dl:// 协议内容,兼容 JSON、命令行风格和纯链接三种格式。"""
result = {"url": "", "headers": {}, "filename": "", "save_dir": ""}
if not raw_url:
return result
# 去掉自定义协议前缀,只保留真正的载荷内容
if raw_url.startswith("m3u8dl://"):
data = raw_url[9:]
elif raw_url.startswith("m3u8dl:"):
data = raw_url[7:]
else:
data = raw_url
data = urllib.parse.unquote(data) # 把 URL 编码内容还原
# 优先解析 JSON 形式,适合浏览器扩展直接传结构化数据
if data.strip().startswith("{"):
try:
json_data = json.loads(data)
result["url"] = json_data.get("url", "")
result["headers"] = json_data.get("headers", {})
result["filename"] = json_data.get("name", "") or json_data.get("filename", "")
return result
except json.JSONDecodeError:
pass
# 如果像 N_m3u8DL-RE 的命令行参数风格,则走专门解析器
if '"' in data or "--" in data or "-H" in data:
log_message("检测到命令行格式协议参数")
return parse_n_m3u8dl_format(data)
# 纯直链场景:直接把首个 http 链接作为下载地址
if data.startswith("http"):
result["url"] = data.split()[0]
return result
# 最后兜底:再尝试一次 JSON,否则整个字符串就当成 URL
try:
json_data = json.loads(data)
result["url"] = json_data.get("url", "")
result["headers"] = json_data.get("headers", {})
result["filename"] = json_data.get("name", "") or json_data.get("filename", "")
return result
except json.JSONDecodeError:
result["url"] = data
return result
7. CatCatch 扩展接入的本地 HTTP 服务,来自 core/catcatch_server.py
[Python] 纯文本查看 复制代码 def do_POST(self):
"""处理浏览器扩展发来的 POST 下载请求。"""
parsed = urlparse(self.path)
if parsed.path != "/download":
self._send_response(404, {"error": "Not found"})
return
content_length = int(self.headers.get("Content-Length", 0))
raw_body = self.rfile.read(content_length)
body_text = raw_body.decode("utf-8", errors="ignore")
content_type = (self.headers.get("Content-Type", "") or "").lower()
data = {}
if body_text:
if "application/json" in content_type:
try:
data = json.loads(body_text) # 标准 JSON 请求体
except json.JSONDecodeError as e:
self._send_response(400, {"error": f"Invalid JSON: {e}"})
return
else:
# 兼容表单风格请求
parsed_form = parse_qs(body_text, keep_blank_values=True)
data = {k: (v[0] if isinstance(v, list) and v else "") for k, v in parsed_form.items()}
url = data.get("url", "")
headers = data.get("headers", {})
filename = data.get("name", "") or data.get("filename", "")
if isinstance(headers, str):
try:
headers = json.loads(headers) # 某些扩展可能把 headers 当字符串传过来,这里再反序列化一次
except json.JSONDecodeError:
headers = {}
if not isinstance(headers, dict):
headers = {}
if url:
self._handle_download_request(url, headers, filename) # 真正转交给主程序下载流程
else:
self._send_response(400, {"error": "Missing 'url' in request body"})
8. 主窗口如何把浏览器、嗅探器、下载管理和资源面板串起来,来自 ui/main_window.py
[Python] 纯文本查看 复制代码 def _init_core_components(self):
"""初始化主窗口背后的核心能力。"""
self.sniffer = M3U8Sniffer() # 负责从浏览器网络请求中识别媒体资源
self.download_manager = DownloadManager(
self.engines,
max_concurrent=config.get("max_concurrent_downloads", 3) # 根据配置决定并发下载数
)
self.download_manager.on_task_update = self._on_task_update # 下载状态变化后,回调 UI 刷新
# 启动本地 HTTP 服务,接收 CatCatch / 协议转发来的下载请求
self.catcatch_server = CatCatchServer(port=config.get("catcatch.port", 9527))
self.catcatch_server.download_requested.connect(self._on_catcatch_download)
self.catcatch_server.start()

小白,所以bug可能会比较多,但是基本的嗅探下载跑了几天都没问题,可以放心使用。
有bug留言,我尽快修复,感谢。
最后得谢谢管理。小白第一次发帖,发了两次都不符合版规,感谢管理耐心的指正。

最后的啰嗦
【手动猪头】 为啥我帖子的表情都不显示图标,都显示成代码了 ?
我觉得将来几年内,针对大型项目或者特殊性的项目,ai依然无法取代高级开发人员。
但对一些短平快小程序,对技术要求不高,反而是对市场需求的敏感性更高,所以学学ai也挺不错。
社区是否考虑设立一个类似于ai开发的板块,不需要啥技术,就看你脑瓜灵光一现,代码交给ai去实现。
自己纯小白,开发小程序只会跟ai口语化交流,专业点的用词表达都不会,最近学会了个词叫“冒烟”。。。。估计在ai看来我就是个傻子,表达都不清楚。。。
最后补充下小白两个多月用ai写代码的流水帐和踩的坑。
最开始写这个小程序,是因为自己想要一个本地的yt-dlp gui程序。
开始是用网页gemini、gpt、deepseek写的,对话轮数多了网页总是卡,也没啥经验,所以最后就做了基本的下载功能套个gui,这算第一版。
然后知道了有antigravity和vs code,发现宝藏了,小白写代码上升到新的高度了,天才程序员快要诞生了!
第一次让ai写超过万行的代码,是想把yt-dlp、aira2、ffmpeg、n_m3u8dl-re、streamlink全都集成到一个后台,做一个全能的下载程序!
于是无知无畏小白的惨案就开始了。。。
让gemini针对这一堆后端出一个开发方案,生成.md文件给我,让ai细化细化细化,最后成了一个两千多行的开发方案。
因为刚开始接触还不知道team羊毛、公益站等,可用的ai资源有限,所以用的开源的qwen3 coder 480b模型。
两千多行的开发方案直接扔给qwen3 coder,我要做的就是是不是的点个继续。。。
就这样兴奋的跑了20多个小时,最后写了2.6万行的代码。
爆雷了,这程序压根跑不起来!
反复修改也不好,总是有问题,甚至有些问题死循环了。
然后扔给antigravity里的opus和gemini,这俩额度快跑完了,依然修复不好。
问ai这种情况怎么解决,ai跟我说修复的成本远高于重新开发,建议我完全重新开发一套。。。那叫一个郁闷!!!
那就重新开发把,把我原来这套两千多行的方案给ai,让他给我一份可行的,不容易出错的开发,这次学会了“分步”开发。
之后跟ai说的就是制定分步开发方案,细化到函数级,感觉自己很懂啊,哈哈
这次不用qwen3 coder了,改用gemini,国产模型差距还是有的。。。
antigravity有时间限额,于是就断断续续的,有额度就写,大概一周左右吧,连写带修复,终于几个后端的基本功能能跑起来了,这是第二版。
当要给第二版继续增加细化功能时,才发现不同的后端之间参数、还有一些规则不能共用,只能共用限速等几个最基本的参数。想把程序做的更完善、人性化等,后面很难继续开发了。
于是就有了第三版,只做前台的集成,后端全部分开独立,这样可以针对每一个后端的特性设置有针对性的参数,功能就可以做的更丰富!
最终这个第三版基本可以满足大多数的下载需求,尤其是调用yt-dlp和n_m3u8dl-re,下载比如b站和流媒体方面,自动判断调用适合的后端等,这一般算是最符合日常使用的一版。
但是想要大而全,还要把每个后端的功能做细做深,对我这个小白来说太难了。。于是就有了第四版。第四版实际是以yt-dlp或n_m3u8dl-re为主,兼顾其他后端的单独开发的两个程序。
这个贴子的程序,是针对n_m3u8dl-re为主的,主要是m3u8、hls等嗅探、下载等,但同时也可以调用yt-dlp等。
截止到现在几乎所有的功能都是让ai给出的,但要把功能细化、更适合自己,下面才算自己真正参与进来。
自己对专业的提示词等也不太懂,第一步跟ai说我要实现什么最基本的目的,ai针对所使用的后端特性搜集整理一份比较全面的功能列表。
然后我会根据市面上主要的类似的程序做一个调研,总结利弊,结合自己的需求列出需要重点开发的功能模块。这一步是最累的,要把ai的那份列表理解,太难了,然后要跟ai反复沟通把我的需求加入到功能列表里。因为涉及到很多规则和逻辑的东西,细化起来巨烧脑。。
功能和基本规则制定好了(其实不算好,只是自己就这点能力了),扔给ai,然后就是制定各种的开发方案,gemini、gpt、opus三个交叉验证,最后生成分步开发方案、执行清单、结项和冒烟记录、测试方案等等等等一堆,扔给ai跑起来!跑完了做各种测试修复。
现在的程序只能保证基础的下载功能没问题,后续全面深入的手工测试我还都没做。。。。
最后含泪给小白一个建议:写中大型项目,用opus或者gpt 5.4 xhigh !!! |
免费评分
-
查看全部评分
|