diff --git a/README.md b/README.md index e69de29..6da4c46 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,45 @@ +# M3U8 Downloader (Go) + +这是一个基于 Go 语言编写的单文件 M3U8 视频下载器练习项目,主要实现了并发下载与本地的混流合成。 + +## ✨ 主要功能 + +- **并发下载**:基于 Goroutine 协程实现 TS 分片的并发拉取。 +- **重试机制**:内置简单的请求重试与失败等待机制。 +- **主列表跳转**:简单的 `#EXT-X-STREAM-INF` 解析与跳转。 +- **FFmpeg 合成**:自动调用本地 FFmpeg 客户端对完成的 TS 视频进行无损的 `.mp4` 混流与清理碎片。 + +## 🚀 快速开始 + +### 1. 准备环境 +- 请确保你的电脑上安装了 **Golang** (建议版本 >= 1.20)。 +- 你的电脑上需要拥有 [FFmpeg](https://ffmpeg.org/download.html)。请将下载好的 FFmpeg 二进制可执行文件(如 `ffmpeg.exe`)放置在本项目根目录下的 `ffmpeg/` 文件夹中。 + - *目录结构应当为:* `m3u8/ffmpeg/ffmpeg.exe` + +### 2. 获取并运行项目 + +```bash +# 1. 克隆本项目代码 +git clone https://github.com/yourusername/m3u8-downloader.git +cd m3u8-downloader + +# 2. 安装项目依赖(命令行进度条组件) +go get github.com/schollz/progressbar/v3 + +# 3. 运行下载器 +go run main.go +``` +*启动后,根据控制台提示粘贴你需要下载的 M3U8 播放列表链接并回车即可。* + +## 🛠️ 二次开发指南 + +如果你想基于此项目进行功能的扩展或深度定制,欢迎 Fork 或修改代码! + +**开发建议与关注点:** +1. **依赖安装**:在进行二次开发或编译之前,请务必确保你已经执行了 `go get github.com/schollz/progressbar/v3` 以拉取进度条相关的必备第三方包。如果提示缺失包,也可以直接运行 `go mod tidy`。 +2. **下载核心逻辑 (`func downLoadOne`)**:由于部分视频网站可能拥有复杂的防盗链机制,你可以在此处为底层的 `http.Client` 请求加入更多的伪装头部,例如自定义的 `User-Agent`、`Referer` 甚至携带鉴权 `Cookie` 和动态 `Token`。 +3. **分片正则爬取 (`func parseSubContent`)**:目前代码通过简单的字符串前后缀判断 (`HasSuffix`) 来定位 `.ts` 分片。遇到无后缀或链接被加密混淆的情况,可以考虑在此处引入强大的“正则表达式”来增强匹配能力。 +4. **进阶:解密与断点保护**:对于带有 `#EXT-X-KEY` 标签的 AES-128 加密视频流,你可以自己为其增加解密模块;或者在文件创建时判断一下之前是否已经拉取过本序号的切片,来实现“断点续传”。 + +## 📄 协议 +[MIT License](LICENSE) diff --git a/ffmpeg/ffmpeg.exe b/ffmpeg/ffmpeg.exe new file mode 100644 index 0000000..5b333ec Binary files /dev/null and b/ffmpeg/ffmpeg.exe differ diff --git a/ffmpeg/ffplay.exe b/ffmpeg/ffplay.exe new file mode 100644 index 0000000..a477000 Binary files /dev/null and b/ffmpeg/ffplay.exe differ diff --git a/ffmpeg/ffprobe.exe b/ffmpeg/ffprobe.exe new file mode 100644 index 0000000..757e0f8 Binary files /dev/null and b/ffmpeg/ffprobe.exe differ diff --git a/main.go b/main.go index 6818ed3..35c5a82 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,7 @@ import ( "io" "net/http" "os" + "os/exec" "strings" "sync" "time" @@ -19,16 +20,19 @@ type M3u8Res struct { } // 创建一个连接池 +// http.Client 是 net/http 包里的核心结构,专门用来处理 HTTP 请求。它包含了一个底层的并发安全连接池 var httpClient = &http.Client{ Transport: &http.Transport{ - MaxIdleConnsPerHost: 5, + MaxIdleConnsPerHost: 100, // 💡 修复1:调大连接池(设为100),避免高并发下被耗尽导致重新握手 }, } // 获取输入的m3u8链接 func getM3u8Path() (path string) { for { + // fmt.Println 是格式化包 fmt 里的输出函数,会自动在末尾加换行 fmt.Println("请输入m3u8链接地址:") + // fmt.Scanln 等待你的终端输入,并把读取到的一行数据存入到 path 变量的内存地址(&path)中 _, err := fmt.Scanln(&path) if err != nil { if err.Error() == "unexpected newline" { @@ -44,16 +48,19 @@ func getM3u8Path() (path string) { // Get链接 func fetchM3u8Content(path string) string { + // 发起一个真正的 HTTP GET 网络请求。 res, err := httpClient.Get(path) if err != nil { fmt.Println(err) return "" } + // defer 用于注册延迟调用,确保这个函数无论怎样返回,都会在最后一步关闭连接释放网络资源,防止内存漏洞 defer res.Body.Close() if res.StatusCode != 200 { fmt.Println("出了点问题", res.StatusCode) return "" } + // io.ReadAll 是标准库 io(输入输出) 里的工具。它可以把网络流数据 (res.Body) 一次性全部读取到内存字节流 body, err := io.ReadAll(res.Body) if err != nil { fmt.Println(err) @@ -63,9 +70,11 @@ func fetchM3u8Content(path string) string { } func parseM3u8(content string, baseUrl string) M3u8Res { + // strings.Split 按照换行符 \n 把长长的文本打散成一个字符串数组,也就是一行一行的 lines := strings.Split(content, "\n") // 优先级最高:检查是否包含 Master 标签 for i, line := range lines { + // strings.Contains 用于检索 line 这句话里面是否包含了某个特定字符串 if strings.Contains(line, "#EXT-X-STREAM-INF") { return parseMainContent(lines, i, baseUrl) } @@ -79,6 +88,7 @@ func parseM3u8(content string, baseUrl string) M3u8Res { func parseMainContent(lines []string, index int, baseUrl string) M3u8Res { // 取匹配标签的下一行作为 URL if index+1 < len(lines) { + // strings.TrimSpace 是去除字符串开头和结尾的空白字符(比如多余的空格或者换行回车符\r) nextUrl := strings.TrimSpace(lines[index+1]) return M3u8Res{ IsMaster: true, @@ -91,10 +101,13 @@ func parseMainContent(lines []string, index int, baseUrl string) M3u8Res { func parseSubContent(lines []string) []string { var tsFiles []string for _, line := range lines { + // 先去除两侧的空格回车 line = strings.TrimSpace(line) + // strings.HasPrefix 判断字符串 line 是不是以 "#" 号开头 if strings.HasPrefix(line, "#") || line == "" { continue } + // strings.HasSuffix 判断字符串的结尾,检查是不是这两种结尾形式 if strings.HasSuffix(line, ".jpeg") || strings.HasSuffix(line, ".ts") { tsFiles = append(tsFiles, line) } @@ -104,6 +117,7 @@ func parseSubContent(lines []string) []string { // 提取基础路径 func getBaseUrl(path string) string { + // strings.LastIndex 返回 "/" 在这个网址里最后一次出现的位置索引下标(数字) lastSlash := strings.LastIndex(path, "/") if lastSlash == -1 { return path + "/" @@ -132,13 +146,17 @@ func buildUrl(baseUrl string, m3u8TsFile []string) []string { } // 协程用 index int 是用来标识每个文件的序号,在并发下载中特别重要 -func downLoadOne(Url string, index int) error { +func downLoadOne(Url string, index int, tempDir string) error { maxRetry := 3 // 定义最大重试次数 + // time.Second 是 time 库里的标准时长常量代表 1 秒钟 baseDelay := time.Second for retry := 0; retry < maxRetry; retry++ { if retry > 0 { + // time.Duration 是时间间隔类型,(1<