吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 1572|回复: 17
收起左侧

[Python 转载] Python + PyQt6 构建面向win系统的流媒体解析和下载工具

  [复制链接]
royswift 发表于 2026-3-27 06:04
本帖最后由 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 !!!

免费评分

参与人数 1吾爱币 +1 热心值 +1 收起 理由
无花 + 1 + 1 用心讨论,共获提升!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

汇成河流 发表于 2026-5-23 08:54
这类工具都有现成的啊,废这么大力气做这么复杂的没啥意义啊,而且只有部分网站是没有做加密的,有加密的需要解决key的问题,现有软件都可以自动解析key的。一般播放速度比较好的都是有加密的,我也做过一个类似的,解析没啥问题,但是下载速度很慢,且速度好的都无法下载,所以后来我做的是解析的网站弄出来给M3U8-Downloader批量下载的格式,一键复制成批量下载的格式,下载起来方便又轻松。不知道你的用起来怎么样,需要安装的支持库太多,我的电脑全部显示安装失败,软件显示组件缺失5,可能是网络问题,你应该把所有支持库打包在里面,但是吧,现成的软件都不大,而且用浏览器扩展就很好用了,又何必这么费劲。
 楼主| royswift 发表于 2026-5-24 23:00
本帖最后由 royswift 于 2026-5-24 23:04 编辑

汇成河流 发表于 2026-5-23 08:54
这类工具都有现成的啊,废这么大力气做这么复杂的没啥意义啊,而且只有部分网站是没有做加密的,有加密的需 ...

默认全部是从github下载的,所以网络要允许。
浏览器扩展猫爪、FetchV、MPMux、Video DownloadHelper,这几个都试了,都不如客户端稳定,但是猫爪的抓取成功率更高,所以就折中猫爪抓取传递给客户端下载。。自己做的初衷,主要是为了抓取成功率更高,同时更适合自己的使用习惯。
tgydslr 发表于 2026-3-27 10:45
bamboorain 发表于 2026-3-27 11:29
很赞叹ai能做出这么有用的工具,赞叹楼主对ai描述需求的能力。请问楼主是用codex还是还是国产大模型呢
mikemelon 发表于 2026-3-27 11:31
支持一下,都哪些网站能用,爱优腾,B站,都能用吗。以前有个叫you-get的命令行工具挺好用的
lyf0720 发表于 2026-3-27 15:21
厉害厉害,你用的是哪一个AI,腾讯的还是豆包的
头像被屏蔽
long679 发表于 2026-3-28 15:48
提示: 作者被禁止或删除 内容自动屏蔽
 楼主| royswift 发表于 2026-3-28 19:06

刚试了,可以哦。再试试。。
 楼主| royswift 发表于 2026-3-28 19:08
bamboorain 发表于 2026-3-27 11:29
很赞叹ai能做出这么有用的工具,赞叹楼主对ai描述需求的能力。请问楼主是用codex还是还是国产大模型呢

gpt 5.4 写的。国产模型我用过qwen3 480b,前面挺好的,但是到最后遇到问题反复解决不了,我纯小白又不懂代码,巨崩溃,后面就改用gpt了。
 楼主| royswift 发表于 2026-3-28 19:09
mikemelon 发表于 2026-3-27 11:31
支持一下,都哪些网站能用,爱优腾,B站,都能用吗。以前有个叫you-get的命令行工具挺好用的

b站可以哦。。爱优腾我没试过哦,国内网站挺严格,我猜大概是不行。
wxpzjt 发表于 2026-3-30 16:57
强。可否共享提示词?正准备研究ai编程 ,我很好奇ai的能力究竟能到什么程度?
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-7-15 01:21

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表