吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 212|回复: 1
收起左侧

[其他转载] Go开发的命令行多线程m3u8下载器,带广告分片过滤功能。

[复制链接]
saobee 发表于 2026-3-17 14:23
这个下载器思路是来自于我本人,只是代码是AI生成,根据版规,AI生成的代码不能用【原创】标记。
已经经过多个版本的优化,目前是已经足够成熟的版本。

【源码放最后】
  • 开发思路:
    m3u8还能有啥思路,就多线程下载分片,然后调用ffmpeg合并。
    用ffmpeg,是因为它是目前最合适的合并m3u8文件为mp4的工具,比自己弄更好吧。
    只要是正常的m3u8网址,不限扩展名,也不用管分片的扩展名,临时目录统统改成.ts
    广告过滤:针对分片网址进行分析,将异常规则,并且时长比例较少的分片判定为广告,不下载。
    其它,就什么多线程之类,请AI做了几次优化。
    同时保存下载分片,也针对未完全下载的分片,加上临时扩展名,防止中途取消后,再次重试时正在下载的几个分片是损坏的。
    还有一个防遗忘的续传批处理,如果中途断开,然后你忘记下载地址了,还可以去临时目录找到一个bat文件,双击即可继续。
    使用了配置文件来保存默认参数,命令行调用时一般就只传递url和文件名就可以了。


* 编译 #太久了,忘记要不要用go get下载包
`m3u8down.exe`这个文件名你可以改成自己想要的。
[Patch] 纯文本查看 复制代码
SETLOCAL
go mod tidy
set CGO_ENABLED=0
set GOOS=windows
set GOARCH=amd64
go build -ldflags="-s -w" -o m3u8down.exe main.go


使用go编译后,直接运行一次,会生成config.ini配置文件


[Plain Text] 纯文本查看 复制代码
[settings]
ffmpeg_path = D:\tools\ffmpeg\bin\ffmpeg.exe #ffmpeg.exe的路径,你也可以用相对路径
show_ffmpeg_log = false # 合并出错了的话,这里开启看ffmpeg输出的内容,找解决方案。
threads     = 12 #线程数,不建议超过32,没必要。
save_dir    = E:\Download	#下载目录
retries     = 5 #重试次数
keep_tmp    = false #下载完成后要不要保留临时片段目录,调试时可以开启
proxy       =  
#proxy 一般留空,需要的时候从命令行传,支持http://127.0.0.1:8080和socks5://127.0.0.1:8081两种格式。
user_agent  = `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36`
# 默认UA,你可以自己随便搞,主要也是方便被识别为正常浏览器
connect_timeout=10  #连接超时时间,秒。
request_timeout=60  #下载分片超时时间,代码里是指下载每256k大小。
full        = false #完整下载整个视频,不过滤广告


启动参数:
[Plain Text] 纯文本查看 复制代码
用法: downloader.exe [选项]
[ 命令行选项 ]
  -u <url>      必填: M3U8 播放列表网址
  -n <name>      可选: 自定义文件名 (不含后缀)
  -d <dir>      可选: 视频保存目录
  -t <num>      可选: 线程数 (覆盖 config.ini)
  -p <proxy>    可选: 代理地址 (http://... 或 socks5://...)
  -e <header>   可选: 附加header,(K1:V1|K2:V2)
  -full         可选: 不进行广告检查,完整下载 (默认: false)
  -h, --help    显示此帮助信息


完整代码,大家自己编译吧:
[Golang] 纯文本查看 复制代码
package main

import (
	"bufio"
	"bytes"
	"context"
	"crypto/md5"
	"encoding/hex"
	"flag"
	"fmt"
	"io"
	"net"
	"net/http"
	"net/url"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"strings"
	"sync"
	"sync/atomic"
	"time"

	"github.com/grafov/m3u8"
	"golang.org/x/net/proxy"
	"gopkg.in/ini.v1"
)

// --- 全局配置与变量 ---
type AppConfig struct {
	FFmpegPath     string
	Threads        int
	SaveDir        string
	Retries        int
	KeepTmp        bool
	ShowFFmpegLog  bool
	Proxy          string
	UserAgent      string
	ConnectTimeout int  // 连接超时 (秒)
	RequestTimeout int  // 单个分片下载超时 (秒)
	FullDownload   bool // 是否完整下载(不检查广告)
}

type SegmentMeta struct {
	URL      string
	KeyURL   string
	KeyIV    string
	Duration float64
	Pattern  string // 路径指纹 (Domain + ParentPath)
}

var (
	globalCfg    AppConfig
	totalBytes   int64   // 全局下载字节计数
	currentSpeed float64 // 全局当前网速 (MB/s)
	exeDir       string  // 程序所在目录
	exeAbsPath   string  // 程序自身绝对路径
)

type progressWriter struct{}

func (pw *progressWriter) Write(p []byte) (n int, err error) {
	n = len(p)
	atomic.AddInt64(&totalBytes, int64(n))
	return n, nil
}

func main() {
	initDefaultConfig()
	uPtr := flag.String("u", "", "M3U8 网址 (URL)")
	nPtr := flag.String("n", "", "保存的文件名 (Name)")
	dPtr := flag.String("d", "", "保存目录 (Directory)")
	tPtr := flag.Int("t", 0, "下载线程数 (Threads)")
	pPtr := flag.String("p", "", "代理地址 (Proxy)")
	ePtr := flag.String("e", "", "额外 Header (K1:V1|K2:V2)")
	fPtr := flag.Bool("full", globalCfg.FullDownload, "是否完整下载,不进行广告检查 (true|false)")
	hPtr := flag.Bool("h", false, "显示详细帮助信息")
	flag.BoolVar(hPtr, "help", false, "显示详细帮助信息")
	flag.Parse()

	if *hPtr {
		showDetailedHelp()
		return
	}

	// 命令行参数覆盖配置
	globalCfg.FullDownload = *fPtr

	if *uPtr != "" {
		runSilentMode(*uPtr, *nPtr, *dPtr, *tPtr, *pPtr, *ePtr)
	} else {
		showWelcomeInfo()
		runInteractiveMode()
	}
}

func runSilentMode(targetUrl, fileName, saveDir string, threads int, proxyAddr string, headerRaw string) {
	if saveDir == "" {
		saveDir = globalCfg.SaveDir
	}
	if threads <= 0 {
		threads = globalCfg.Threads
	}
	if proxyAddr == "" {
		proxyAddr = globalCfg.Proxy
	}
	executeTask(targetUrl, saveDir, fileName, threads, proxyAddr, headerRaw)
}

func runInteractiveMode() {
	scanner := bufio.NewScanner(os.Stdin)
	var targetUrl, savePath, fileName, headerRaw, proxyAddr string
	threads := globalCfg.Threads

	fmt.Println(">>> 交互模式 (直接回车使用默认值) <<<")
	fmt.Print("1. M3U8 网址 (必填): ")
	if scanner.Scan() {
		targetUrl = strings.TrimSpace(scanner.Text())
	}
	if targetUrl == "" {
		return
	}

	fmt.Printf("2. 保存目录 (默认: %s): ", globalCfg.SaveDir)
	if scanner.Scan() {
		savePath = strings.TrimSpace(scanner.Text())
		if savePath == "" {
			savePath = globalCfg.SaveDir
		}
	}

	fmt.Print("3. 自定义文件名: ")
	if scanner.Scan() {
		fileName = strings.TrimSpace(scanner.Text())
	}

	fmt.Printf("4. 线程数 (默认: %d): ", threads)
	if scanner.Scan() {
		tText := strings.TrimSpace(scanner.Text())
		if tText != "" {
			fmt.Sscanf(tText, "%d", &threads)
		}
	}

	fmt.Printf("5. 代理 (当前: %s): ", globalCfg.Proxy)
	if scanner.Scan() {
		proxyAddr = strings.TrimSpace(scanner.Text())
		if proxyAddr == "" {
			proxyAddr = globalCfg.Proxy
		}
	}

	fmt.Print("6. 额外 Header (K1:V1|K2:V2): ")
	if scanner.Scan() {
		headerRaw = strings.TrimSpace(scanner.Text())
	}

	executeTask(targetUrl, savePath, fileName, threads, proxyAddr, headerRaw)

	fmt.Println("\n-------------------------------------------------------")
	fmt.Println("任务结束。按 [回车键] 退出程序...")
	bufio.NewReader(os.Stdin).ReadBytes('\n')
}

func executeTask(targetUrl, savePath, fileName string, threads int, proxyAddr, headerRaw string) {
	atomic.StoreInt64(&totalBytes, 0)
	currentSpeed = 0
	urlHash := getMD5(targetUrl)
	fileNameBase := chooseFileName(fileName, urlHash)

	// 创建带连接池和超时的 HTTP Client
	httpClient := createHttpClient(proxyAddr, threads)

	finalSaveBase := savePath
	if !filepath.IsAbs(finalSaveBase) {
		finalSaveBase = filepath.Join(exeDir, finalSaveBase)
	}

	fmt.Println("-------------------------------------------------------")
	fmt.Printf("&#128194; 保存目录: %s\n", finalSaveBase)
	fmt.Printf("&#128196; 目标文件: %s.mp4 (时长解析中...)\n", fileNameBase)
	if proxyAddr != "" {
		fmt.Printf("&#127760; 使用代理: %s\n", proxyAddr)
	} else {
		fmt.Println("&#127760; 直连模式 (无代理)")
	}
	fmt.Println("-------------------------------------------------------")

	headers := make(map[string]string)
	headers["User-Agent"] = globalCfg.UserAgent
	if headerRaw != "" {
		for _, p := range strings.Split(headerRaw, "|") {
			kv := strings.SplitN(p, ":", 2)
			if len(kv) == 2 {
				headers[strings.TrimSpace(kv[0])] = strings.TrimSpace(kv[1])
			}
		}
	}

	fmt.Println("[1/3] 解析 M3U8 并重算时长 (处理分片)...")
	// 修正调用:传入 globalCfg.FullDownload
	segments, duration := selectAndParse(targetUrl, httpClient, headers, globalCfg.FullDownload)
	if len(segments) == 0 {
		fmt.Println("&#10060; 错误:无法解析有效分片。")
		return
	}

	durationTag := formatDuration(duration)
	fileNameFinal := fileNameBase + "[" + durationTag + "]"
	finalFilePath := filepath.Join(finalSaveBase, fileNameFinal+".mp4")
	fmt.Printf("&#127919; 最终路径: %s\n", finalFilePath)

	tmpDir := filepath.Join(finalSaveBase, "tmp", urlHash)
	os.MkdirAll(tmpDir, 0755)

	batPath := filepath.Join(tmpDir, "resume_download.bat")
	var batBuilder strings.Builder
	batBuilder.WriteString(fmt.Sprintf("\"%s\" ", exeAbsPath))
	batBuilder.WriteString(fmt.Sprintf("-u \"%s\" ", targetUrl))
	if fileName != "" {
		batBuilder.WriteString(fmt.Sprintf("-n \"%s\" ", fileName))
	}
	batBuilder.WriteString(fmt.Sprintf("-d \"%s\" ", finalSaveBase))
	batBuilder.WriteString(fmt.Sprintf("-t %d ", threads))
	if globalCfg.FullDownload {
		batBuilder.WriteString("-full=true ")
	}
	if proxyAddr != "" {
		batBuilder.WriteString(fmt.Sprintf("-p %s ", proxyAddr))
	}
	if headerRaw != "" {
		batBuilder.WriteString(fmt.Sprintf("-e \"%s\" ", headerRaw))
	}
	os.WriteFile(batPath, []byte(batBuilder.String()), 0644)

	stopSpeedometer := make(chan bool)
	go func() {
		var lastBytes int64
		var history []float64
		ticker := time.NewTicker(time.Second)
		defer ticker.Stop()
		for {
			select {
			case <-ticker.C:
				nowBytes := atomic.LoadInt64(&totalBytes)
				delta := nowBytes - lastBytes
				if delta < 0 {
					delta = 0
				}
				instantSpeed := float64(delta) / 1024 / 1024
				history = append(history, instantSpeed)
				if len(history) > 3 {
					history = history[1:]
				}
				var sum float64
				for _, v := range history {
					sum += v
				}
				currentSpeed = sum / float64(len(history))
				lastBytes = nowBytes
			case <-stopSpeedometer:
				return
			}
		}
	}()

	keyMap := make(map[string]string)
	for _, seg := range segments {
		if seg.KeyURL != "" {
			if _, exists := keyMap[seg.KeyURL]; !exists {
				localKeyName := fmt.Sprintf("key_%d.key", len(keyMap))
				keyPath := filepath.Join(tmpDir, localKeyName)
				retryDownload(httpClient, seg.KeyURL, keyPath, headers, globalCfg.Retries)
				keyMap[seg.KeyURL] = localKeyName
			}
		}
	}

	total := len(segments)
	taskChan := make(chan int, total)
	var wg sync.WaitGroup
	var mu sync.Mutex
	var activeThreads int32
	completed := 0
	startTime := time.Now()

	fmt.Printf("[2/3] 下载分片: %d | 时长: %s | 续传脚本: tmp\\%s\\resume_download.bat\n", total, durationTag, urlHash)
	for i := 0; i < threads; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for idx := range taskChan {
				atomic.AddInt32(&activeThreads, 1)
				tsPath := filepath.Join(tmpDir, fmt.Sprintf("%s_%d.ts", urlHash, idx+1))
				if info, err := os.Stat(tsPath); err == nil && info.Size() > 0 {
					// 已存在则跳过
				} else {
					retryDownload(httpClient, segments[idx].URL, tsPath, headers, globalCfg.Retries)
				}
				atomic.AddInt32(&activeThreads, -1)
				mu.Lock()
				completed++
				percentage := float64(completed) / float64(total)
				var etaStr string = "--:--"
				if percentage > 0 && currentSpeed > 0 {
					estimatedTotalBytes := float64(atomic.LoadInt64(&totalBytes)) / percentage
					remainingBytes := estimatedTotalBytes - float64(atomic.LoadInt64(&totalBytes))
					etaSeconds := remainingBytes / (currentSpeed * 1024 * 1024)
					if etaSeconds > 0 && etaSeconds < 360000 {
						etaStr = fmt.Sprintf("%02d:%02d", int(etaSeconds)/60, int(etaSeconds)%60)
					}
					_ = time.Since(startTime).Seconds()
				}
				fmt.Printf("\r&#128640; 进度: [%d/%d] %.1f%% | 已下: %.1f MB | 速度: %.2f MB/s | 剩余: %s | 活动: %d/%d      ",
					completed, total, percentage*100, float64(atomic.LoadInt64(&totalBytes))/1024/1024, currentSpeed, etaStr, atomic.LoadInt32(&activeThreads), threads)
				mu.Unlock()
			}
		}()
	}

	for i := 0; i < total; i++ {
		taskChan <- i
	}
	close(taskChan)
	wg.Wait()
	stopSpeedometer <- true
	currentSpeed = 0

	localM3u8Path := filepath.Join(tmpDir, "index.m3u8")
	writeLocalM3U8(localM3u8Path, urlHash, segments, keyMap)

	fmt.Printf("\n[3/3] FFmpeg 合并中...")
	cmdArgs := []string{
		"-loglevel", "warning",
		"-protocol_whitelist", "file,http,https,tcp,tls,crypto",
		"-allowed_extensions", "ALL",
		"-fflags", "+genpts+igndts",
		"-i", localM3u8Path,
		"-c", "copy",
		"-bsf:a", "aac_adtstoasc",
		"-y", finalFilePath,
	}
	if globalCfg.ShowFFmpegLog {
		cmdArgs[1] = "info"
	}
	ffmpeg := globalCfg.FFmpegPath
	if !filepath.IsAbs(ffmpeg) {
		if _, err := os.Stat(filepath.Join(exeDir, ffmpeg)); err == nil {
			ffmpeg = filepath.Join(exeDir, ffmpeg)
		}
	}
	cmd := exec.Command(ffmpeg, cmdArgs...)
	if globalCfg.ShowFFmpegLog {
		cmd.Stdout = os.Stdout
		cmd.Stderr = os.Stderr
	}
	if err := cmd.Run(); err == nil {
		fmt.Println("\n&#9989; 任务完成:", finalFilePath)
		if !globalCfg.KeepTmp {
			os.RemoveAll(tmpDir)
		}
	} else {
		fmt.Printf("\n&#10060; 合并失败。")
	}
}

// 1. 核心解析:只负责提取数据,不进行任何剔除
func parseSegments(mUrl string, client *http.Client, headers map[string]string) ([]SegmentMeta, error) {
	req, _ := http.NewRequest("GET", mUrl, nil)
	for k, v := range headers {
		req.Header.Set(k, v)
	}

	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	bodyBytes, _ := io.ReadAll(resp.Body)
	p, listType, err := m3u8.DecodeFrom(bytes.NewReader(bodyBytes), true)
	if err != nil {
		return nil, err
	}

	baseUrl, _ := url.Parse(mUrl)

	// 处理 Master Playlist
	if listType == m3u8.MASTER {
		master := p.(*m3u8.MasterPlaylist)
		var activeVariant *m3u8.Variant
		for _, v := range master.Variants {
			if v != nil {
				activeVariant = v
				break
			}
		}
		if activeVariant == nil {
			return nil, fmt.Errorf("no variants found")
		}
		subUrl, _ := url.Parse(activeVariant.URI)
		return parseSegments(baseUrl.ResolveReference(subUrl).String(), client, headers)
	}

	// 处理 Media Playlist
	if listType == m3u8.MEDIA {
		mpl := p.(*m3u8.MediaPlaylist)
		var segs []SegmentMeta
		curKeyURL, curKeyIV := "", ""

		for _, v := range mpl.Segments {
			if v == nil {
				continue
			}

			// AES-128 密钥处理
			if v.Key != nil {
				if v.Key.Method == "AES-128" {
					ku, _ := url.Parse(v.Key.URI)
					curKeyURL = baseUrl.ResolveReference(ku).String()
					curKeyIV = v.Key.IV
				} else {
					curKeyURL = ""
					curKeyIV = ""
				}
			}

			u, _ := url.Parse(v.URI)
			fullUrl := baseUrl.ResolveReference(u).String()

			// 生成路径指纹
			pathParts := strings.Split(u.Path, "/")
			pattern := u.Host
			if len(pathParts) > 1 {
				pattern += strings.Join(pathParts[:len(pathParts)-1], "/")
			}

			segs = append(segs, SegmentMeta{
				URL:      fullUrl,
				KeyURL:   curKeyURL,
				KeyIV:    curKeyIV,
				Duration: v.Duration,
				Pattern:  pattern,
			})
		}
		return segs, nil
	}

	return nil, fmt.Errorf("unknown playlist type")
}

// 2. 智能过滤:根据重复、域名和路径特征进行分析
func filterAds(segs []SegmentMeta) []SegmentMeta {
	if len(segs) <= 3 {
		return segs
	}

	seenUrls := make(map[string]bool)
	var uniqueSegs []SegmentMeta
	repeatCount := 0
	for _, s := range segs {
		if seenUrls[s.URL] {
			repeatCount++
			continue
		}
		seenUrls[s.URL] = true
		uniqueSegs = append(uniqueSegs, s)
	}
	if repeatCount > 0 {
		fmt.Printf("&#9888;&#65039; [去重] 过滤了 %d 个重复分片\n", repeatCount)
	}

	patternDuration := make(map[string]float64)
	for _, s := range uniqueSegs {
		patternDuration[s.Pattern] += s.Duration
	}

	var mainPattern string
	var maxDuration float64
	for p, dur := range patternDuration {
		if dur > maxDuration {
			maxDuration = dur
			mainPattern = p
		}
	}

	var clean []SegmentMeta
	adCount := 0
	for _, s := range uniqueSegs {
		if s.Pattern == mainPattern || patternDuration[s.Pattern] > (maxDuration*0.2) {
			clean = append(clean, s)
		} else {
			adCount++
		}
	}

	if adCount > 0 {
		fmt.Printf("&#127919; [智能拦截] 基于路径指纹和频率分析,已剔除 %d 个广告分片\n", adCount)
	}

	return clean
}

// 3. 入口函数:控制流程并汇总结果
func selectAndParse(mUrl string, client *http.Client, headers map[string]string, keepAds bool) ([]SegmentMeta, float64) {
	allSegs, err := parseSegments(mUrl, client, headers)
	if err != nil {
		fmt.Printf("&#10060; 解析失败: %v\n", err)
		return nil, 0
	}

	var finalSegs []SegmentMeta
	if keepAds {
		fmt.Println("&#8505;&#65039; 模式: 完整下载 (已跳过广告检测)")
		finalSegs = allSegs
	} else {
		finalSegs = filterAds(allSegs)
	}

	var totalDur float64
	for _, s := range finalSegs {
		totalDur += s.Duration
	}

	return finalSegs, totalDur
}

func retryDownload(client *http.Client, u, path string, headers map[string]string, maxRetries int) {
	tmpPath := path + ".tmp"

	for i := 0; i <= maxRetries; i++ {
		ctx, cancel := context.WithTimeout(context.Background(), time.Duration(globalCfg.RequestTimeout)*time.Second)

		req, _ := http.NewRequestWithContext(ctx, "GET", u, nil)
		for k, v := range headers {
			req.Header.Set(k, v)
		}

		resp, err := client.Do(req)
		if err == nil && resp.StatusCode == 200 {
			f, err := os.Create(tmpPath)
			if err != nil {
				cancel()
				continue
			}
			bufWriter := bufio.NewWriterSize(f, 256*1024)
			tee := io.TeeReader(resp.Body, &progressWriter{})

			n, copyErr := io.Copy(bufWriter, tee)
			flushErr := bufWriter.Flush()
			f.Close()
			resp.Body.Close()
			cancel()

			if copyErr == nil && flushErr == nil {
				if strings.HasSuffix(path, ".key") && n > 16 {
					d, _ := os.ReadFile(tmpPath)
					os.WriteFile(path, d[:16], 0644)
					os.Remove(tmpPath)
				} else {
					os.Rename(tmpPath, path)
				}
				return
			}
			os.Remove(tmpPath)
		}

		if resp != nil {
			resp.Body.Close()
		}
		cancel()
		time.Sleep(time.Duration(i+1) * time.Second)
	}
}

func writeLocalM3U8(path, hash string, segs []SegmentMeta, keyMap map[string]string) {
	f, _ := os.Create(path)
	defer f.Close()
	f.WriteString("#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:60\n")
	lastK := "INITIAL_STATE"
	for i, s := range segs {
		curK := keyMap[s.KeyURL]
		if curK != lastK {
			if lastK != "INITIAL_STATE" {
				f.WriteString("#EXT-X-DISCONTINUITY\n")
			}
			if curK != "" {
				iv := ""
				if s.KeyIV != "" {
					iv = ",IV=" + s.KeyIV
				}
				fmt.Fprintf(f, "#EXT-X-KEY:METHOD=AES-128,URI=\"%s\"%s\n", curK, iv)
			} else {
				f.WriteString("#EXT-X-KEY:METHOD=NONE\n")
			}
			lastK = curK
		}
		fmt.Fprintf(f, "#EXTINF:%.3f,\n%s_%d.ts\n", s.Duration, hash, i+1)
	}
	f.WriteString("#EXT-X-ENDLIST")
}

func initDefaultConfig() {
	p, err := os.Executable()
	if err != nil {
		p = "."
	}
	exeAbsPath, _ = filepath.Abs(p)
	exeDir = filepath.Dir(exeAbsPath)
	configPath := filepath.Join(exeDir, "config.ini")

	globalCfg = AppConfig{
		FFmpegPath:     "ffmpeg.exe",
		Threads:        16,
		SaveDir:        "videos",
		Retries:        5,
		UserAgent:      "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0",
		ConnectTimeout: 10,
		RequestTimeout: 30,
		FullDownload:   false,
	}

	cfg, err := ini.Load(configPath)
	if err == nil {
		sec := cfg.Section("settings")
		globalCfg.FFmpegPath = sec.Key("ffmpeg_path").MustString(globalCfg.FFmpegPath)
		globalCfg.Threads = sec.Key("threads").MustInt(globalCfg.Threads)
		globalCfg.SaveDir = sec.Key("save_dir").MustString(globalCfg.SaveDir)
		globalCfg.Retries = sec.Key("retries").MustInt(globalCfg.Retries)
		globalCfg.KeepTmp = sec.Key("keep_tmp").MustBool(false)
		globalCfg.ShowFFmpegLog = sec.Key("show_ffmpeg_log").MustBool(false)
		globalCfg.Proxy = sec.Key("proxy").MustString("")
		globalCfg.UserAgent = sec.Key("user_agent").MustString(globalCfg.UserAgent)
		globalCfg.ConnectTimeout = sec.Key("connect_timeout").MustInt(globalCfg.ConnectTimeout)
		globalCfg.RequestTimeout = sec.Key("request_timeout").MustInt(globalCfg.RequestTimeout)
		globalCfg.FullDownload = sec.Key("full_download").MustBool(false)
	} else {
		cfg := ini.Empty()
		sec := cfg.Section("settings")
		sec.Key("ffmpeg_path").SetValue(globalCfg.FFmpegPath)
		sec.Key("threads").SetValue(fmt.Sprint(globalCfg.Threads))
		sec.Key("save_dir").SetValue(globalCfg.SaveDir)
		sec.Key("retries").SetValue(fmt.Sprint(globalCfg.Retries))
		sec.Key("keep_tmp").SetValue("false")
		sec.Key("show_ffmpeg_log").SetValue("false")
		sec.Key("proxy").SetValue("")
		sec.Key("user_agent").SetValue(globalCfg.UserAgent)
		sec.Key("connect_timeout").SetValue(fmt.Sprint(globalCfg.ConnectTimeout))
		sec.Key("request_timeout").SetValue(fmt.Sprint(globalCfg.RequestTimeout))
		sec.Key("full_download").SetValue("false")
		cfg.SaveTo(configPath)
	}
}

func showDetailedHelp() {
	fmt.Println(`
M3U8 Downloader CLI 帮助手册
-------------------------------------------------------
用法: downloader.exe [选项]
[ 命令行选项 ]
  -u <url>      必填: M3U8 播放列表网址
  -n <name>      可选: 自定义文件名 (不含后缀)
  -d <dir>      可选: 视频保存目录
  -t <num>      可选: 线程数 (覆盖 config.ini)
  -p <proxy>    可选: 代理地址 (http://... 或 socks5://...)
  -e <header>   可选: 附加header,(K1:V1|K2:V2)
  -full         可选: 不进行广告检查,完整下载 (默认: false)
  -h, --help    显示此帮助信息
-------------------------------------------------------`)
}

func showWelcomeInfo() {
	fmt.Println("=======================================================")
	fmt.Println("          M3U8 下载器 v3.0 (Optimization Edition)      ")
	fmt.Println("=======================================================\n")
}

func getMD5(t string) string {
	h := md5.Sum([]byte(t))
	return hex.EncodeToString(h[:])
}

func chooseFileName(input, hash string) string {
	if input == "" {
		return hash
	}
	re := regexp.MustCompile(`[\\/:*?"<>|]`)
	clean := re.ReplaceAllString(input, "_")
	clean = strings.TrimSpace(clean)
	if clean == "" {
		return hash
	}
	return clean
}

func formatDuration(s float64) string {
	return fmt.Sprintf("%02d_%02d", int(s)/60, int(s)%60)
}

func createHttpClient(p string, threads int) *http.Client {
	dialer := &net.Dialer{
		Timeout:   time.Duration(globalCfg.ConnectTimeout) * time.Second,
		KeepAlive: 30 * time.Second,
	}

	t := &http.Transport{
		DialContext:           dialer.DialContext,
		ForceAttemptHTTP2:     true,
		MaxIdleConns:          100,
		IdleConnTimeout:       90 * time.Second,
		TLSHandshakeTimeout:   10 * time.Second,
		ExpectContinueTimeout: 1 * time.Second,
		MaxIdleConnsPerHost:   threads + 2,
	}

	if p != "" {
		u, _ := url.Parse(p)
		if strings.HasPrefix(u.Scheme, "http") {
			t.Proxy = http.ProxyURL(u)
		} else if strings.HasPrefix(u.Scheme, "socks5") {
			d, _ := proxy.FromURL(u, proxy.Direct)
			t.Dial = d.Dial
		}
	}

	return &http.Client{
		Transport: t,
	}
}

免费评分

参与人数 1吾爱币 +1 热心值 +1 收起 理由
KKBon + 1 + 1 谢谢@Thanks!

查看全部评分

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

qq410559442 发表于 2026-3-18 02:20
感谢分享
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

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

GMT+8, 2026-3-18 02:30

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

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