package main import ( "fmt" "io" "net/http" "os" "os/exec" "strings" "sync" "time" "github.com/schollz/progressbar/v3" ) type M3u8Res struct { IsMaster bool NextUrl string TsFile []string } // 创建一个连接池 // http.Client 是 net/http 包里的核心结构,专门用来处理 HTTP 请求。它包含了一个底层的并发安全连接池 var httpClient = &http.Client{ Transport: &http.Transport{ 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" { fmt.Println("你啥也没输入") continue } fmt.Println(err) continue } return } } // 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) return "" } return string(body) } 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) } } return M3u8Res{ IsMaster: false, TsFile: parseSubContent(lines), } } 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, NextUrl: baseUrl + nextUrl, } } return M3u8Res{IsMaster: false} } 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) } } return tsFiles } // 提取基础路径 func getBaseUrl(path string) string { // strings.LastIndex 返回 "/" 在这个网址里最后一次出现的位置索引下标(数字) lastSlash := strings.LastIndex(path, "/") if lastSlash == -1 { return path + "/" } return path[:lastSlash+1] } //func getNewBaseUrl(path string) string { // lastSlash := strings.LastIndex(path, "/") // if lastSlash == -1 { // return path + "/" // } // subPath := path[:lastSlash] // secondLastSlash := strings.LastIndex(subPath, "/") // return path[:secondLastSlash+1] //} // 拼接下载地址 func buildUrl(baseUrl string, m3u8TsFile []string) []string { var downLoadUrl []string for _, tsUrl := range m3u8TsFile { fullUrl := baseUrl + tsUrl downLoadUrl = append(downLoadUrl, fullUrl) } return downLoadUrl } // 协程用 index int 是用来标识每个文件的序号,在并发下载中特别重要 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<