ALL
This commit is contained in:
10
.idea/.gitignore
generated
vendored
Normal file
10
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# 默认忽略的文件
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# 已忽略包含查询文件的默认文件夹
|
||||||
|
/queries/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
|
# 基于编辑器的 HTTP 客户端请求
|
||||||
|
/httpRequests/
|
||||||
7
.idea/dictionaries/project.xml
generated
Normal file
7
.idea/dictionaries/project.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<component name="ProjectDictionaryState">
|
||||||
|
<dictionary name="project">
|
||||||
|
<words>
|
||||||
|
<w>outputpath</w>
|
||||||
|
</words>
|
||||||
|
</dictionary>
|
||||||
|
</component>
|
||||||
11
.idea/go.imports.xml
generated
Normal file
11
.idea/go.imports.xml
generated
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="GoImports">
|
||||||
|
<option name="excludedPackages">
|
||||||
|
<array>
|
||||||
|
<option value="github.com/pkg/errors" />
|
||||||
|
<option value="golang.org/x/net/context" />
|
||||||
|
</array>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
9
.idea/m3u8.iml
generated
Normal file
9
.idea/m3u8.iml
generated
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="Go" enabled="true" />
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/m3u8.iml" filepath="$PROJECT_DIR$/.idea/m3u8.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
11
go.mod
Normal file
11
go.mod
Normal file
@@ -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
|
||||||
|
)
|
||||||
10
go.sum
Normal file
10
go.sum
Normal file
@@ -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=
|
||||||
263
main.go
Normal file
263
main.go
Normal file
@@ -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<<uint(retry-1)) // 1s, 2s, 4s
|
||||||
|
fmt.Printf("[%3d] 第 %d 次重试,等待 %v...\n", index, retry, waitTime)
|
||||||
|
time.Sleep(waitTime)
|
||||||
|
}
|
||||||
|
resp, err := httpClient.Get(Url)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("连接失败: %v\n", err)
|
||||||
|
if retry < maxRetry-1 {
|
||||||
|
continue // 重试
|
||||||
|
}
|
||||||
|
return fmt.Errorf("(%d)连接失败 %d次后: %v", index, maxRetry, err)
|
||||||
|
}
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
resp.Body.Close()
|
||||||
|
if retry < maxRetry-1 {
|
||||||
|
continue // 重试
|
||||||
|
}
|
||||||
|
return fmt.Errorf("服务器状态异常: %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
fileName := fmt.Sprintf("%d", index)
|
||||||
|
file, err := os.Create(fileName)
|
||||||
|
if err != nil {
|
||||||
|
resp.Body.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = io.Copy(file, resp.Body)
|
||||||
|
closeErr := file.Close()
|
||||||
|
resp.Body.Close()
|
||||||
|
if err != nil || closeErr != nil {
|
||||||
|
os.Remove(fileName)
|
||||||
|
if err != nil {
|
||||||
|
if retry < maxRetry-1 {
|
||||||
|
continue // 重试
|
||||||
|
}
|
||||||
|
return fmt.Errorf("下载中断: %v %v", err, index)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("文件关闭失败: %v", closeErr)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 并行下载函数
|
||||||
|
func downLoadALL(fullUrl []string, maxConcurrent int) {
|
||||||
|
var wait sync.WaitGroup
|
||||||
|
total := len(fullUrl)
|
||||||
|
bar := progressbar.Default(int64(total), "正在下载(%d)")
|
||||||
|
maxT := make(chan struct{}, maxConcurrent) //定义一个长度为maxConcurrent的信道
|
||||||
|
wait.Add(len(fullUrl))
|
||||||
|
for i := range fullUrl {
|
||||||
|
maxT <- struct{}{} // 写入一个信号
|
||||||
|
go func(url string, index int) {
|
||||||
|
defer func() {
|
||||||
|
<-maxT
|
||||||
|
bar.Add(1)
|
||||||
|
}()
|
||||||
|
defer wait.Done()
|
||||||
|
err := downLoadOne(url, index)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("文件 %d 失败: %v ", index, err)
|
||||||
|
}
|
||||||
|
}(fullUrl[i], i)
|
||||||
|
}
|
||||||
|
wait.Wait()
|
||||||
|
bar.Finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 碎片拼接
|
||||||
|
func videoBuild(num int, outputpath string, deleteAfter bool) error {
|
||||||
|
output, err := os.Create(outputpath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer output.Close()
|
||||||
|
|
||||||
|
for i := 0; i < num; i++ {
|
||||||
|
fileName := fmt.Sprintf("%d", i)
|
||||||
|
input, err := os.Open(fileName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("打开文件失败\n")
|
||||||
|
}
|
||||||
|
writtenBytes, err := io.Copy(output, input)
|
||||||
|
input.Close()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("合并文件失败\n")
|
||||||
|
}
|
||||||
|
if deleteAfter {
|
||||||
|
os.Remove(fileName)
|
||||||
|
}
|
||||||
|
fmt.Printf("合并成功,%d\n", writtenBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
m3u8Path := getM3u8Path() // 输入链接
|
||||||
|
m3u8Body := fetchM3u8Content(m3u8Path) // get到链接文件
|
||||||
|
m3u8BaseUrl := getBaseUrl(m3u8Path) // 获取基础路径
|
||||||
|
m3u8TsFile := parseM3u8(m3u8Body, m3u8BaseUrl)
|
||||||
|
if m3u8TsFile.IsMaster {
|
||||||
|
fmt.Println("检测到主播放列表,准备跳转:", m3u8TsFile.NextUrl)
|
||||||
|
baseNew := getBaseUrl(m3u8TsFile.NextUrl) // 新的基础路径
|
||||||
|
m3u8Body = fetchM3u8Content(m3u8TsFile.NextUrl) // get新的内容
|
||||||
|
m3u8TsFileNew := parseM3u8(m3u8Body, baseNew)
|
||||||
|
fullUrl := buildUrl(baseNew, m3u8TsFileNew.TsFile)
|
||||||
|
downLoadALL(fullUrl, 10)
|
||||||
|
fmt.Printf("下载完成?开始合并...\n")
|
||||||
|
videoBuild(len(m3u8TsFileNew.TsFile), "test.mp4", true)
|
||||||
|
} else {
|
||||||
|
fmt.Println("获得分片列表,准备下载:", len(m3u8TsFile.TsFile), "个文件")
|
||||||
|
fullUrl := buildUrl(m3u8BaseUrl, m3u8TsFile.TsFile)
|
||||||
|
downLoadALL(fullUrl, 10)
|
||||||
|
fmt.Printf("下载完成?开始合并...\n")
|
||||||
|
videoBuild(len(m3u8TsFile.TsFile), "test.mp4", true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//http://devimages.apple.com/iphone/samples/bipbop/gear1/prog_index.m3u8
|
||||||
|
//https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8
|
||||||
|
//https://test-streams.mux.dev/x36xhzz/url_0/193039199_mp4_h264_aac_hd_7.m3u8
|
||||||
|
//https://surrit.com/0831edf7-98aa-49ff-a865-42777726e282/720p/video.m3u8
|
||||||
Reference in New Issue
Block a user