main v1.2
orestonce 2022-06-12 22:43:20 +08:00
parent 7f7c452559
commit a83bfbfd61
10 changed files with 220 additions and 203 deletions

View File

@ -1,7 +1,7 @@
## m3u8d 一款m3u8下载工具
* 提供windows图形界面(Qt), mac\linux命令行, linux支持arm和386
* 使用ffmpeg转换格式为mp4
* windows自带GUI界面的版本下载: [m3u8d-qt_v1.1_windows_amd64.exe](https://github.com/orestonce/m3u8d/releases/download/v1.1/m3u8d-qt_v1.1_windows_amd64.exe):
* windows自带GUI界面的版本下载: [m3u8d-qt_v1.2_windows_amd64.exe](https://github.com/orestonce/m3u8d/releases/download/v1.2/m3u8d-qt_v1.2_windows_amd64.exe):
![](m3u8d-qt/screenshot.png)
* 全部版本下载, 包括windows图形界面/linux命令行/mac命令行: https://github.com/orestonce/m3u8d/releases

View File

@ -1,14 +1,14 @@
package m3u8d
import (
"bytes"
"context"
"crypto/aes"
"crypto/cipher"
"crypto/sha256"
"crypto/tls"
"encoding/hex"
"errors"
"fmt"
"github.com/levigross/grequests"
"github.com/orestonce/goffmpeg"
"github.com/orestonce/gopool"
"io"
@ -43,6 +43,7 @@ func GetProgress() int {
type RunDownload_Resp struct {
ErrMsg string
IsSkipped bool
IsCancel bool
SaveFileTo string
}
@ -55,7 +56,15 @@ type RunDownload_Req struct {
SkipTsCountFromHead int `json:",omitempty"` // 跳过前面几个ts
}
func RunDownload(req RunDownload_Req) (resp RunDownload_Resp) {
type downloadEnv struct {
cancelFn func()
ctx context.Context
client *http.Client
header http.Header
}
func (this *downloadEnv) RunDownload(req RunDownload_Req) (resp RunDownload_Resp) {
if req.HostType == "" {
req.HostType = "apiv1"
}
@ -73,10 +82,23 @@ func RunDownload(req RunDownload_Req) (resp RunDownload_Resp) {
if req.SkipTsCountFromHead < 0 {
req.SkipTsCountFromHead = 0
}
var err error
req.M3u8Url, err = sniffM3u8(req.M3u8Url)
host, err := getHost(req.M3u8Url, "apiv2")
if err != nil {
resp.ErrMsg = "getHost0: " + err.Error()
return resp
}
this.header = http.Header{
"User-Agent": []string{"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36"},
"Connection": []string{"keep-alive"},
"Accept": []string{"*/*"},
"Accept-Encoding": []string{"*"},
"Accept-Language": []string{"zh-CN,zh;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5"},
"Referer": []string{host},
}
req.M3u8Url, err = this.sniffM3u8(req.M3u8Url)
if err != nil {
resp.ErrMsg = "sniffM3u8: " + err.Error()
resp.IsCancel = this.GetIsCancel()
return resp
}
id, err := req.getVideoId()
@ -103,23 +125,6 @@ func RunDownload(req RunDownload_Req) (resp RunDownload_Resp) {
resp.ErrMsg = "SetupFfmpeg error: " + err.Error()
return resp
}
host, err := getHost(req.M3u8Url, "apiv2")
if err != nil {
resp.ErrMsg = "getHost0: " + err.Error()
return resp
}
ro := &grequests.RequestOptions{
UserAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36",
RequestTimeout: 10 * time.Second, //请求头超时时间
Headers: map[string]string{
"Connection": "keep-alive",
"Accept": "*/*",
"Accept-Encoding": "*",
"Accept-Language": "zh-CN,zh;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5",
},
}
ro.Headers["Referer"] = host
ro.InsecureSkipVerify = req.Insecure
if !strings.HasPrefix(req.M3u8Url, "http") || req.M3u8Url == "" {
resp.ErrMsg = "M3u8Url not valid " + strconv.Quote(req.M3u8Url)
return resp
@ -135,35 +140,38 @@ func RunDownload(req RunDownload_Req) (resp RunDownload_Resp) {
m3u8Host, err := getHost(req.M3u8Url, req.HostType)
if err != nil {
resp.ErrMsg = "getHost1: " + err.Error()
resp.IsCancel = this.GetIsCancel()
return resp
}
// 获取m3u8地址的内容体
r, err := grequests.Get(req.M3u8Url, ro)
m3u8Body, err := this.doGetRequest(req.M3u8Url)
if err != nil {
resp.ErrMsg = "getM3u8Body: " + err.Error()
resp.IsCancel = this.GetIsCancel()
return resp
}
m3u8Body := r.String()
ts_key, err := getM3u8Key(ro, m3u8Host, m3u8Body)
ts_key, err := this.getM3u8Key(m3u8Host, string(m3u8Body))
if err != nil {
resp.ErrMsg = "getM3u8Key: " + err.Error()
resp.IsCancel = this.GetIsCancel()
return resp
}
ts_list := getTsList(m3u8Host, m3u8Body)
if len(ts_list) <= req.SkipTsCountFromHead {
tsList := getTsList(m3u8Host, string(m3u8Body))
if len(tsList) <= req.SkipTsCountFromHead {
resp.ErrMsg = "需要下载的文件为空"
return resp
}
ts_list = ts_list[req.SkipTsCountFromHead:]
tsList = tsList[req.SkipTsCountFromHead:]
// 下载ts
err = downloader(ro, ts_list, downloadDir, ts_key)
err = this.downloader(tsList, downloadDir, ts_key)
if err != nil {
resp.ErrMsg = "下载ts文件错误: " + err.Error()
resp.IsCancel = this.GetIsCancel()
return resp
}
DrawProgressBar(1)
var tsFileList []string
for _, one := range ts_list {
for _, one := range tsList {
tsFileList = append(tsFileList, filepath.Join(downloadDir, one.Name))
}
var tmpOutputName string
@ -223,6 +231,40 @@ func RunDownload(req RunDownload_Req) (resp RunDownload_Resp) {
return resp
}
var gOldEnv *downloadEnv
var gOldEnvLocker sync.Mutex
func RunDownload(req RunDownload_Req) (resp RunDownload_Resp) {
env := &downloadEnv{
client: &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: req.Insecure,
},
},
Timeout: time.Second * 10,
},
}
env.ctx, env.cancelFn = context.WithCancel(context.Background())
gOldEnvLocker.Lock()
if gOldEnv != nil {
gOldEnv.cancelFn()
}
gOldEnv = env
gOldEnvLocker.Unlock()
return env.RunDownload(req)
}
func CloseOldEnv() {
gOldEnvLocker.Lock()
defer gOldEnvLocker.Unlock()
if gOldEnv != nil {
gOldEnv.cancelFn()
}
gOldEnv = nil
}
// 获取m3u8地址的host
func getHost(Url, ht string) (host string, err error) {
u, err := url.Parse(Url)
@ -245,7 +287,7 @@ func GetWd() string {
}
// 获取m3u8加密的密钥
func getM3u8Key(ro *grequests.RequestOptions, host, html string) (key string, err error) {
func (this *downloadEnv) getM3u8Key(host, html string) (key string, err error) {
lines := strings.Split(html, "\n")
key = ""
for _, line := range lines {
@ -256,16 +298,14 @@ func getM3u8Key(ro *grequests.RequestOptions, host, html string) (key string, er
if !strings.Contains(line, "http") {
key_url = fmt.Sprintf("%s/%s", host, key_url)
}
res, err := grequests.Get(key_url, ro)
res, err := this.doGetRequest(key_url)
if err != nil {
return "", err
}
if res.StatusCode == 200 {
key = res.String()
}
return string(res), nil
}
}
return key, nil
return "", nil
}
func getTsList(host, body string) (tsList []TsInfo) {
@ -300,33 +340,18 @@ func getTsList(host, body string) (tsList []TsInfo) {
// 下载ts文件
// @modify: 2020-08-13 修复ts格式SyncByte合并不能播放问题
func downloadTsFile(ro *grequests.RequestOptions, ts TsInfo, download_dir, key string) (err error) {
func (this *downloadEnv) downloadTsFile(ts TsInfo, download_dir, key string) (err error) {
currPath := fmt.Sprintf("%s/%s", download_dir, ts.Name)
if isFileExists(currPath) {
return nil
}
res, err := grequests.Get(ts.Url, ro)
data, err := this.doGetRequest(ts.Url)
if err != nil {
return err
}
if !res.Ok {
return errors.New("!res.Ok")
}
// 校验长度是否合法
var origData []byte
origData = res.Bytes()
contentLen := 0
contentLenStr := res.Header.Get("Content-Length")
if contentLenStr != "" {
contentLen, _ = strconv.Atoi(contentLenStr)
}
if len(origData) == 0 || (contentLen > 0 && len(origData) < contentLen) || res.Error != nil {
msg := ""
if res.Error != nil {
msg = res.Error.Error()
}
return errors.New("[warn] File: " + ts.Name + "res origData invalid or err" + msg)
}
origData = data
// 解密出视频 ts 源文件
if key != "" {
//解密 ts 文件算法aes 128 cbc pack5
@ -354,7 +379,14 @@ func downloadTsFile(ro *grequests.RequestOptions, ts TsInfo, download_dir, key s
return os.Rename(tmpPath, currPath)
}
func downloader(ro *grequests.RequestOptions, tsList []TsInfo, downloadDir string, key string) (err error) {
func (this *downloadEnv) SleepDur(d time.Duration) {
select {
case <-time.After(d):
case <-this.ctx.Done():
}
}
func (this *downloadEnv) downloader(tsList []TsInfo, downloadDir string, key string) (err error) {
task := gopool.NewThreadPool(8)
tsLen := len(tsList)
downloadCount := 0
@ -371,10 +403,16 @@ func downloader(ro *grequests.RequestOptions, tsList []TsInfo, downloadDir strin
return
}
locker.Unlock()
lastErr = downloadTsFile(ro, ts, downloadDir, key)
if i > 0 {
this.SleepDur(time.Second * time.Duration(i))
}
lastErr = this.downloadTsFile(ts, downloadDir, key)
if lastErr == nil {
break
}
if this.GetIsCancel() {
break
}
}
if lastErr != nil {
locker.Lock()
@ -428,64 +466,21 @@ func isDirExists(path string) bool {
return err == nil && stat.IsDir()
}
// 判断文件是否存在
func pathExists(path string) bool {
_, err := os.Stat(path)
if err == nil {
return true
}
if os.IsNotExist(err) {
return false
}
return false
}
// ============================== 加解密相关 ==============================
func PKCS7Padding(ciphertext []byte, blockSize int) []byte {
padding := blockSize - len(ciphertext)%blockSize
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
return append(ciphertext, padtext...)
}
func PKCS7UnPadding(origData []byte) []byte {
length := len(origData)
unpadding := int(origData[length-1])
return origData[:(length - unpadding)]
}
func AesEncrypt(origData, key []byte, ivs ...[]byte) ([]byte, error) {
func AesDecrypt(crypted, key []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
blockSize := block.BlockSize()
var iv []byte
if len(ivs) == 0 {
iv = key
} else {
iv = ivs[0]
}
origData = PKCS7Padding(origData, blockSize)
blockMode := cipher.NewCBCEncrypter(block, iv[:blockSize])
crypted := make([]byte, len(origData))
blockMode.CryptBlocks(crypted, origData)
return crypted, nil
}
func AesDecrypt(crypted, key []byte, ivs ...[]byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
blockSize := block.BlockSize()
var iv []byte
if len(ivs) == 0 {
iv = key
} else {
iv = ivs[0]
}
blockMode := cipher.NewCBCDecrypter(block, iv[:blockSize])
blockMode := cipher.NewCBCDecrypter(block, key[:blockSize])
origData := make([]byte, len(crypted))
blockMode.CryptBlocks(origData, crypted)
origData = PKCS7UnPadding(origData)
@ -508,17 +503,11 @@ func getFileSha256(targetFile string) (v string) {
return hex.EncodeToString(tmp[:])
}
func sniffM3u8(urlS string) (afterUrl string, err error) {
func (this *downloadEnv) sniffM3u8(urlS string) (afterUrl string, err error) {
if strings.HasSuffix(strings.ToLower(urlS), ".m3u8") {
return urlS, nil
}
resp, err := http.DefaultClient.Get(urlS)
if err != nil {
return "", err
}
defer resp.Body.Close()
content, err := io.ReadAll(resp.Body)
content, err := this.doGetRequest(urlS)
if err != nil {
return "", err
}
@ -528,3 +517,32 @@ func sniffM3u8(urlS string) (afterUrl string, err error) {
}
return string(groups[0]), nil
}
func (this *downloadEnv) doGetRequest(urlS string) (data []byte, err error) {
req, err := http.NewRequest(http.MethodGet, urlS, nil)
if err != nil {
return nil, err
}
req = req.WithContext(this.ctx)
req.Header = this.header
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
content, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return content, nil
}
func (this *downloadEnv) GetIsCancel() bool {
select {
case <-this.ctx.Done():
return true
default:
return false
}
}

View File

@ -39,7 +39,7 @@ func BuildCliBinary() {
},
}
for _, cfg := range list {
name := "m3u8d_cli_v1.1_" + cfg.GOOS + "_" + cfg.GOARCH + cfg.Ext
name := "m3u8d_cli_v1.2_" + cfg.GOOS + "_" + cfg.GOARCH + cfg.Ext
cmd := exec.Command("go", "build", "-o", filepath.Join(wd, "bin", name))
cmd.Dir = filepath.Join(wd, "cmd")
cmd.Env = append(os.Environ(), "GOOS="+cfg.GOOS)
@ -62,6 +62,7 @@ func CreateLibForQtUi() {
EnableQtClass_Toast: true,
})
ctx.Generate1(m3u8d.RunDownload)
ctx.Generate1(m3u8d.CloseOldEnv)
ctx.Generate1(m3u8d.GetProgress)
ctx.Generate1(m3u8d.GetWd)
ctx.MustCreateAmd64LibraryInDir("m3u8d-qt")

Binary file not shown.

View File

@ -70,6 +70,7 @@ extern "C" {
#endif
extern __declspec(dllexport) void Go2cppFn_RunDownload(char* in, int inLen, char** out, int* outLen);
extern __declspec(dllexport) void Go2cppFn_CloseOldEnv(char* in, int inLen, char** out, int* outLen);
extern __declspec(dllexport) void Go2cppFn_GetProgress(char* in, int inLen, char** out, int* outLen);
extern __declspec(dllexport) void Go2cppFn_GetWd(char* in, int inLen, char** out, int* outLen);
@ -82,53 +83,53 @@ RunDownload_Resp RunDownload(RunDownload_Req in0){
std::string in;
{
{
uint32_t tmp18 = in0.M3u8Url.length();
char tmp19[4];
tmp19[0] = (uint32_t(tmp18) >> 24) & 0xFF;
tmp19[1] = (uint32_t(tmp18) >> 16) & 0xFF;
tmp19[2] = (uint32_t(tmp18) >> 8) & 0xFF;
tmp19[3] = (uint32_t(tmp18) >> 0) & 0xFF;
in.append(tmp19, 4);
uint32_t tmp19 = in0.M3u8Url.length();
char tmp20[4];
tmp20[0] = (uint32_t(tmp19) >> 24) & 0xFF;
tmp20[1] = (uint32_t(tmp19) >> 16) & 0xFF;
tmp20[2] = (uint32_t(tmp19) >> 8) & 0xFF;
tmp20[3] = (uint32_t(tmp19) >> 0) & 0xFF;
in.append(tmp20, 4);
in.append(in0.M3u8Url);
}
{
uint32_t tmp20 = in0.HostType.length();
char tmp21[4];
tmp21[0] = (uint32_t(tmp20) >> 24) & 0xFF;
tmp21[1] = (uint32_t(tmp20) >> 16) & 0xFF;
tmp21[2] = (uint32_t(tmp20) >> 8) & 0xFF;
tmp21[3] = (uint32_t(tmp20) >> 0) & 0xFF;
in.append(tmp21, 4);
uint32_t tmp21 = in0.HostType.length();
char tmp22[4];
tmp22[0] = (uint32_t(tmp21) >> 24) & 0xFF;
tmp22[1] = (uint32_t(tmp21) >> 16) & 0xFF;
tmp22[2] = (uint32_t(tmp21) >> 8) & 0xFF;
tmp22[3] = (uint32_t(tmp21) >> 0) & 0xFF;
in.append(tmp22, 4);
in.append(in0.HostType);
}
in.append((char*)(&in0.Insecure), 1);
{
uint32_t tmp22 = in0.SaveDir.length();
char tmp23[4];
tmp23[0] = (uint32_t(tmp22) >> 24) & 0xFF;
tmp23[1] = (uint32_t(tmp22) >> 16) & 0xFF;
tmp23[2] = (uint32_t(tmp22) >> 8) & 0xFF;
tmp23[3] = (uint32_t(tmp22) >> 0) & 0xFF;
in.append(tmp23, 4);
uint32_t tmp23 = in0.SaveDir.length();
char tmp24[4];
tmp24[0] = (uint32_t(tmp23) >> 24) & 0xFF;
tmp24[1] = (uint32_t(tmp23) >> 16) & 0xFF;
tmp24[2] = (uint32_t(tmp23) >> 8) & 0xFF;
tmp24[3] = (uint32_t(tmp23) >> 0) & 0xFF;
in.append(tmp24, 4);
in.append(in0.SaveDir);
}
{
uint32_t tmp24 = in0.FileName.length();
char tmp25[4];
tmp25[0] = (uint32_t(tmp24) >> 24) & 0xFF;
tmp25[1] = (uint32_t(tmp24) >> 16) & 0xFF;
tmp25[2] = (uint32_t(tmp24) >> 8) & 0xFF;
tmp25[3] = (uint32_t(tmp24) >> 0) & 0xFF;
in.append(tmp25, 4);
uint32_t tmp25 = in0.FileName.length();
char tmp26[4];
tmp26[0] = (uint32_t(tmp25) >> 24) & 0xFF;
tmp26[1] = (uint32_t(tmp25) >> 16) & 0xFF;
tmp26[2] = (uint32_t(tmp25) >> 8) & 0xFF;
tmp26[3] = (uint32_t(tmp25) >> 0) & 0xFF;
in.append(tmp26, 4);
in.append(in0.FileName);
}
{
char tmp26[4];
tmp26[0] = (uint32_t(in0.SkipTsCountFromHead) >> 24) & 0xFF;
tmp26[1] = (uint32_t(in0.SkipTsCountFromHead) >> 16) & 0xFF;
tmp26[2] = (uint32_t(in0.SkipTsCountFromHead) >> 8) & 0xFF;
tmp26[3] = (uint32_t(in0.SkipTsCountFromHead) >> 0) & 0xFF;
in.append(tmp26, 4);
char tmp27[4];
tmp27[0] = (uint32_t(in0.SkipTsCountFromHead) >> 24) & 0xFF;
tmp27[1] = (uint32_t(in0.SkipTsCountFromHead) >> 16) & 0xFF;
tmp27[2] = (uint32_t(in0.SkipTsCountFromHead) >> 8) & 0xFF;
tmp27[3] = (uint32_t(in0.SkipTsCountFromHead) >> 0) & 0xFF;
in.append(tmp27, 4);
}
}
char *out = NULL;
@ -138,28 +139,30 @@ RunDownload_Resp RunDownload(RunDownload_Req in0){
int outIdx = 0;
{
{
uint32_t tmp27 = 0;
uint32_t tmp28 = uint32_t(uint8_t(out[outIdx+0]) << 24);
uint32_t tmp29 = uint32_t(uint8_t(out[outIdx+1]) << 16);
uint32_t tmp30 = uint32_t(uint8_t(out[outIdx+2]) << 8);
uint32_t tmp31 = uint32_t(uint8_t(out[outIdx+3]) << 0);
tmp27 = tmp28 | tmp29 | tmp30 | tmp31;
uint32_t tmp28 = 0;
uint32_t tmp29 = uint32_t(uint8_t(out[outIdx+0]) << 24);
uint32_t tmp30 = uint32_t(uint8_t(out[outIdx+1]) << 16);
uint32_t tmp31 = uint32_t(uint8_t(out[outIdx+2]) << 8);
uint32_t tmp32 = uint32_t(uint8_t(out[outIdx+3]) << 0);
tmp28 = tmp29 | tmp30 | tmp31 | tmp32;
outIdx+=4;
retValue.ErrMsg = std::string(out+outIdx, out+outIdx+tmp27);
outIdx+=tmp27;
retValue.ErrMsg = std::string(out+outIdx, out+outIdx+tmp28);
outIdx+=tmp28;
}
retValue.IsSkipped = (bool) out[outIdx];
outIdx++;
retValue.IsCancel = (bool) out[outIdx];
outIdx++;
{
uint32_t tmp32 = 0;
uint32_t tmp33 = uint32_t(uint8_t(out[outIdx+0]) << 24);
uint32_t tmp34 = uint32_t(uint8_t(out[outIdx+1]) << 16);
uint32_t tmp35 = uint32_t(uint8_t(out[outIdx+2]) << 8);
uint32_t tmp36 = uint32_t(uint8_t(out[outIdx+3]) << 0);
tmp32 = tmp33 | tmp34 | tmp35 | tmp36;
uint32_t tmp33 = 0;
uint32_t tmp34 = uint32_t(uint8_t(out[outIdx+0]) << 24);
uint32_t tmp35 = uint32_t(uint8_t(out[outIdx+1]) << 16);
uint32_t tmp36 = uint32_t(uint8_t(out[outIdx+2]) << 8);
uint32_t tmp37 = uint32_t(uint8_t(out[outIdx+3]) << 0);
tmp33 = tmp34 | tmp35 | tmp36 | tmp37;
outIdx+=4;
retValue.SaveFileTo = std::string(out+outIdx, out+outIdx+tmp32);
outIdx+=tmp32;
retValue.SaveFileTo = std::string(out+outIdx, out+outIdx+tmp33);
outIdx+=tmp33;
}
}
if (out != NULL) {
@ -168,6 +171,16 @@ RunDownload_Resp RunDownload(RunDownload_Req in0){
return retValue;
}
void CloseOldEnv(){
std::string in;
char *out = NULL;
int outLen = 0;
Go2cppFn_CloseOldEnv((char *)in.data(), in.length(), &out, &outLen);
if (out != NULL) {
free(out);
}
}
int32_t GetProgress(){
std::string in;
char *out = NULL;

View File

@ -17,9 +17,11 @@ struct RunDownload_Req{
struct RunDownload_Resp{
std::string ErrMsg;
bool IsSkipped;
bool IsCancel;
std::string SaveFileTo;
};
RunDownload_Resp RunDownload(RunDownload_Req in0);
void CloseOldEnv();
int32_t GetProgress();
std::string GetWd();

View File

@ -20,6 +20,7 @@ MainWindow::MainWindow(QWidget *parent) :
MainWindow::~MainWindow()
{
CloseOldEnv();
delete ui;
}
@ -35,6 +36,8 @@ void MainWindow::on_pushButton_RunDownload_clicked()
ui->checkBox_Insecure->setEnabled(false);
ui->progressBar->setValue(0);
ui->pushButton_RunDownload->setText("正在下载...");
ui->pushButton_StopDownload->setEnabled(true);
m_syncUi.AddRunFnOn_OtherThread([this](){
// isFinished被 other thread 和 ui thread共享
std::shared_ptr<std::atomic_bool> isFinished = std::make_shared<std::atomic_bool>(false);
@ -72,6 +75,10 @@ void MainWindow::on_pushButton_RunDownload_clicked()
ui->pushButton_RunDownload->setEnabled(true);
ui->checkBox_Insecure->setEnabled(false);
ui->pushButton_RunDownload->setText("开始下载");
ui->pushButton_StopDownload->setEnabled(false);
if (resp.IsCancel) {
return;
}
if (!resp.ErrMsg.empty()) {
Toast::Instance()->SetError(QString::fromStdString(resp.ErrMsg));
@ -91,3 +98,8 @@ void MainWindow::on_pushButton_SaveDir_clicked()
QString dir = QFileDialog::getExistingDirectory(this);
ui->lineEdit_SaveDir->setText(dir);
}
void MainWindow::on_pushButton_StopDownload_clicked()
{
CloseOldEnv();
}

View File

@ -21,6 +21,8 @@ private slots:
void on_pushButton_SaveDir_clicked();
void on_pushButton_StopDownload_clicked();
private:
Ui::MainWindow *ui;
RunOnUiThread m_syncUi;

View File

@ -112,7 +112,7 @@
</spacer>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout" stretch="0,1,0">
<layout class="QHBoxLayout" name="horizontalLayout" stretch="0,1,0,0">
<item>
<widget class="QLabel" name="label_3">
<property name="text">
@ -134,6 +134,16 @@
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_StopDownload">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>停止下载</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>

View File

@ -1,41 +0,0 @@
package m3u8d
import (
"crypto/sha256"
"encoding/hex"
"io"
"os"
)
func mergeTsFileList_Raw(tsFileList []string, outputTs string) (hash string, err error) {
fout, err := os.Create(outputTs)
if err != nil {
return "", err
}
hashWriter := sha256.New()
multiW := io.MultiWriter(fout, hashWriter)
for _, tsName := range tsFileList {
var fin *os.File
fin, err = os.Open(tsName)
if err != nil {
fout.Close()
_ = os.Remove(outputTs)
return "", err
}
_, err = io.Copy(multiW, fin)
if err != nil {
fin.Close()
fout.Close()
_ = os.Remove(outputTs)
return "", err
}
fin.Close()
}
err = fout.Close()
if err != nil {
return "", err
}
tmp := hashWriter.Sum(nil)
return hex.EncodeToString(tmp[:]), nil
}