diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..4647a46
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,10 @@
+# 默认忽略的文件
+/shelf/
+/workspace.xml
+# 已忽略包含查询文件的默认文件夹
+/queries/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
+# 基于编辑器的 HTTP 客户端请求
+/httpRequests/
diff --git a/.idea/dictionaries/project.xml b/.idea/dictionaries/project.xml
new file mode 100644
index 0000000..e485277
--- /dev/null
+++ b/.idea/dictionaries/project.xml
@@ -0,0 +1,7 @@
+
+
+
+ outputpath
+
+
+
\ No newline at end of file
diff --git a/.idea/go.imports.xml b/.idea/go.imports.xml
new file mode 100644
index 0000000..b893f17
--- /dev/null
+++ b/.idea/go.imports.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/m3u8.iml b/.idea/m3u8.iml
new file mode 100644
index 0000000..338a266
--- /dev/null
+++ b/.idea/m3u8.iml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..2e4c3d6
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..a00d452
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,11 @@
+module m3u8-downloader
+
+go 1.25.0
+
+require (
+ github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
+ github.com/rivo/uniseg v0.4.7 // indirect
+ github.com/schollz/progressbar/v3 v3.19.0 // indirect
+ golang.org/x/sys v0.41.0 // indirect
+ golang.org/x/term v0.40.0 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..3aad9e3
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,10 @@
+github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
+github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
+github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
+github.com/schollz/progressbar/v3 v3.19.0 h1:Ea18xuIRQXLAUidVDox3AbwfUhD0/1IvohyTutOIFoc=
+github.com/schollz/progressbar/v3 v3.19.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec=
+golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
+golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
+golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..6818ed3
--- /dev/null
+++ b/main.go
@@ -0,0 +1,263 @@
+package main
+
+import (
+ "fmt"
+ "io"
+ "net/http"
+ "os"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/schollz/progressbar/v3"
+)
+
+type M3u8Res struct {
+ IsMaster bool
+ NextUrl string
+ TsFile []string
+}
+
+// 创建一个连接池
+var httpClient = &http.Client{
+ Transport: &http.Transport{
+ MaxIdleConnsPerHost: 5,
+ },
+}
+
+// 获取输入的m3u8链接
+func getM3u8Path() (path string) {
+ for {
+ fmt.Println("请输入m3u8链接地址:")
+ _, 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 {
+ res, err := httpClient.Get(path)
+ if err != nil {
+ fmt.Println(err)
+ return ""
+ }
+ defer res.Body.Close()
+ if res.StatusCode != 200 {
+ fmt.Println("出了点问题", res.StatusCode)
+ return ""
+ }
+ body, err := io.ReadAll(res.Body)
+ if err != nil {
+ fmt.Println(err)
+ return ""
+ }
+ return string(body)
+}
+
+func parseM3u8(content string, baseUrl string) M3u8Res {
+ lines := strings.Split(content, "\n")
+ // 优先级最高:检查是否包含 Master 标签
+ for i, line := range lines {
+ 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) {
+ 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)
+ if strings.HasPrefix(line, "#") || line == "" {
+ continue
+ }
+ if strings.HasSuffix(line, ".jpeg") || strings.HasSuffix(line, ".ts") {
+ tsFiles = append(tsFiles, line)
+ }
+ }
+ return tsFiles
+}
+
+// 提取基础路径
+func getBaseUrl(path string) string {
+ 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) error {
+ maxRetry := 3 // 定义最大重试次数
+ baseDelay := time.Second
+ for retry := 0; retry < maxRetry; retry++ {
+ if retry > 0 {
+ waitTime := baseDelay * time.Duration(1<