@@ -2,113 +2,321 @@ package m3u8d
2
2
3
3
import (
4
4
"context"
5
+ "crypto/tls"
5
6
"io/ioutil"
7
+ "net/http"
8
+ "net/url"
9
+ "os"
6
10
"path/filepath"
7
- "sort "
11
+ "strconv "
8
12
"strings"
9
- "sync"
13
+ "sync/atomic"
14
+ "time"
10
15
)
11
16
12
- var gMergeStatus SpeedStatus
13
- var gMergeCancel context. CancelFunc
14
- var gMergeCancelLocker sync. Mutex
17
+ func ( this * DownloadEnv ) StartDownload ( req StartDownload_Req ) ( errMsg string ) {
18
+ this . status . Locker . Lock ()
19
+ defer this . status . Locker . Unlock ()
15
20
16
- type MergeTsDir_Resp struct {
17
- ErrMsg string
18
- IsCancel bool
21
+ if this .status .IsRunning {
22
+ errMsg = "正在下载"
23
+ return errMsg
24
+ }
25
+
26
+ var proxyUrlObj * url.URL
27
+ req .SetProxy , proxyUrlObj , errMsg = ParseProxyFormat (req .SetProxy )
28
+ if errMsg != "" {
29
+ return errMsg
30
+ }
31
+ this .setupClient (req , proxyUrlObj )
32
+ errMsg = this .prepareReqAndHeader (& req )
33
+ if errMsg != "" {
34
+ return errMsg
35
+ }
36
+
37
+ this .status .clearStatusNoLock ()
38
+
39
+ this .status .progressBarShow = req .ProgressBarShow
40
+ this .ctx , this .cancelFn = context .WithCancel (context .Background ())
41
+ this .status .IsRunning = true
42
+ go func () {
43
+ this .runDownload (req )
44
+ this .status .SetProgressBarTitle ("下载进度" )
45
+ this .status .DrawProgressBar (1 , 0 )
46
+
47
+ this .status .Locker .Lock ()
48
+ this .cancelFn ()
49
+ this .status .IsRunning = false
50
+ this .status .Locker .Unlock ()
51
+ }()
52
+ return ""
19
53
}
20
54
21
- var gMergeIsRunning bool
22
- var gMergeIsRunningLocker sync. Mutex
55
+ func ( this * DownloadEnv ) GetStatus () ( resp GetStatus_Resp ) {
56
+ var sleepTh int32
23
57
24
- func beginMerge () bool {
25
- gMergeIsRunningLocker .Lock ()
26
- defer gMergeIsRunningLocker .Unlock ()
58
+ sleepTh = atomic .LoadInt32 (& this .sleepTh )
59
+ resp .Percent = this .status .GetPercent ()
60
+ resp .Title = this .status .GetTitle ()
61
+ if resp .Title == "" {
62
+ resp .Title = "正在下载"
63
+ }
64
+ {
65
+ this .status .Locker .Lock ()
66
+ resp .IsDownloading = this .status .IsRunning
67
+ resp .ErrMsg = this .status .errMsg
68
+ resp .IsSkipped = this .status .isSkipped
69
+ resp .SaveFileTo = this .status .saveFileTo
70
+ this .status .Locker .Unlock ()
71
+ }
72
+
73
+ var speed SpeedInfo
74
+ if resp .IsDownloading {
75
+ speed = this .status .SpeedRecent5sGetAndUpdate ()
76
+ }
77
+ resp .StatusBar = speed .ToString ()
78
+ if sleepTh > 0 {
79
+ if resp .StatusBar != "" {
80
+ resp .StatusBar += ", "
81
+ }
82
+ resp .StatusBar += "有 " + strconv .Itoa (int (sleepTh )) + "个线程正在休眠."
83
+ }
27
84
28
- if gMergeIsRunning != false {
29
- return false
85
+ if resp . ErrMsg != "" {
86
+ resp . IsCancel = this . GetIsCancel ()
30
87
}
31
- gMergeIsRunning = true
32
- return true
88
+ return resp
33
89
}
34
90
35
- func MergeTsDir (InputTsDir string , OutputMp4Name string ) (resp MergeTsDir_Resp ) {
36
- if ! beginMerge () {
37
- return resp
91
+ func (this * DownloadEnv ) WaitDownloadFinish () GetStatus_Resp {
92
+ for {
93
+ status := this .GetStatus ()
94
+ if status .IsDownloading {
95
+ time .Sleep (time .Millisecond * 100 )
96
+ continue
97
+ }
98
+ return status
38
99
}
100
+ }
39
101
40
- defer func ( ) {
41
- gMergeIsRunningLocker .Lock ()
42
- gMergeIsRunning = false
43
- gMergeIsRunningLocker .Unlock ()
44
- }()
102
+ func ( this * DownloadEnv ) setErrMsg ( errMsg string ) {
103
+ this . status . Locker .Lock ()
104
+ this . status . errMsg = errMsg
105
+ this . status . Locker .Unlock ()
106
+ }
45
107
46
- fList , err := ioutil .ReadDir (InputTsDir )
108
+ func (this * DownloadEnv ) setSaveFileTo (to string , isSkipped bool ) {
109
+ this .status .Locker .Lock ()
110
+ this .status .saveFileTo = to
111
+ this .status .isSkipped = isSkipped
112
+ this .status .Locker .Unlock ()
113
+ }
114
+
115
+ func (this * DownloadEnv ) runDownload (req StartDownload_Req ) {
116
+ this .status .SetProgressBarTitle ("[1/6]嗅探m3u8" )
117
+ var m3u8Body []byte
118
+ var errMsg string
119
+ req .M3u8Url , m3u8Body , errMsg = this .sniffM3u8 (req .M3u8Url )
120
+ if errMsg != "" {
121
+ this .setErrMsg ("sniffM3u8: " + errMsg )
122
+ return
123
+ }
124
+ videoId , err := req .getVideoId ()
47
125
if err != nil {
48
- resp . ErrMsg = "读取目录失败 " + err .Error ()
126
+ this . setErrMsg ( "getVideoId: " + err .Error () )
49
127
return
50
128
}
51
- var tsFileList []string
52
- for _ , f := range fList {
53
- if f .Mode ().IsRegular () && strings .HasSuffix (strings .ToLower (f .Name ()), ".ts" ) {
54
- tsFileList = append (tsFileList , filepath .Join (InputTsDir , f .Name ()))
129
+
130
+ if req .SkipCacheCheck == false {
131
+ var info * DbVideoInfo
132
+ info , err = cacheRead (req .SaveDir , videoId )
133
+ if err != nil {
134
+ this .setErrMsg ("cacheRead: " + err .Error ())
135
+ return
136
+ }
137
+ if info != nil {
138
+ this .status .SetProgressBarTitle ("[2/6]检查是否已下载" )
139
+ latestNameFullPath , found := info .SearchVideoInDir (req .SaveDir )
140
+ if found {
141
+ this .setSaveFileTo (latestNameFullPath , true )
142
+ return
143
+ }
55
144
}
56
145
}
57
- if len ( tsFileList ) == 0 {
58
- resp . ErrMsg = "目录下不存在ts文件: " + InputTsDir
146
+ if ! strings . HasPrefix ( req . M3u8Url , "http" ) || req . M3u8Url == "" {
147
+ this . setErrMsg ( "M3u8Url not valid " + strconv . Quote ( req . M3u8Url ))
59
148
return
60
149
}
61
- sort .Strings (tsFileList ) // 按照字典顺序排序
62
- if OutputMp4Name == "" {
63
- OutputMp4Name = filepath .Join (InputTsDir , "all.mp4" )
64
- } else if ! filepath .IsAbs (OutputMp4Name ) {
65
- OutputMp4Name = filepath .Join (InputTsDir , OutputMp4Name )
150
+ downloadDir := filepath .Join (req .SaveDir , "downloading" , videoId )
151
+ if ! isDirExists (downloadDir ) {
152
+ err := os .MkdirAll (downloadDir , os .ModePerm )
153
+ if err != nil {
154
+ this .setErrMsg ("os.MkdirAll error: " + err .Error ())
155
+ return
156
+ }
157
+ }
158
+
159
+ downloadingFilePath := filepath .Join (downloadDir , "downloading.txt" )
160
+ if ! isFileExists (downloadingFilePath ) {
161
+ err = ioutil .WriteFile (downloadingFilePath , []byte (req .M3u8Url ), 0666 )
162
+ if err != nil {
163
+ this .setErrMsg ("os.WriteUrl error: " + err .Error ())
164
+ return
165
+ }
66
166
}
67
- ctx , cancelFn := context .WithCancel (context .Background ())
68
- defer cancelFn ()
69
167
70
- gMergeCancelLocker .Lock ()
71
- if gMergeCancel != nil {
72
- gMergeCancel ()
168
+ beginSeq := parseBeginSeq (m3u8Body )
169
+ // 获取m3u8地址的内容体
170
+ encInfo , err := this .getEncryptInfo (req .M3u8Url , string (m3u8Body ))
171
+ if err != nil {
172
+ this .setErrMsg ("getEncryptInfo: " + err .Error ())
173
+ return
174
+ }
175
+ this .status .SetProgressBarTitle ("[3/6]获取ts列表" )
176
+ tsList , errMsg := getTsList (beginSeq , req .M3u8Url , string (m3u8Body ))
177
+ if errMsg != "" {
178
+ this .setErrMsg ("获取ts列表错误: " + errMsg )
179
+ return
73
180
}
74
- gMergeCancel = cancelFn
75
- gMergeCancelLocker .Unlock ()
181
+ if len (tsList ) <= req .SkipTsCountFromHead {
182
+ this .setErrMsg ("需要下载的文件为空" )
183
+ return
184
+ }
185
+ tsList = tsList [req .SkipTsCountFromHead :]
186
+ // 下载ts
187
+ this .status .SetProgressBarTitle ("[4/6]下载ts" )
188
+ this .status .SpeedResetBytes ()
189
+ err = this .downloader (tsList , downloadDir , encInfo , req .ThreadCount )
190
+ this .status .SpeedResetBytes ()
191
+ if err != nil {
192
+ this .setErrMsg ("下载ts文件错误: " + err .Error ())
193
+ return
194
+ }
195
+ this .status .DrawProgressBar (1 , 1 )
196
+ var tsFileList []string
197
+ for _ , one := range tsList {
198
+ tsFileList = append (tsFileList , filepath .Join (downloadDir , one .Name ))
199
+ }
200
+ var tmpOutputName string
201
+ tmpOutputName = filepath .Join (downloadDir , "all.merge.mp4" )
76
202
203
+ this .status .SetProgressBarTitle ("[5/6]合并ts为mp4" )
77
204
err = MergeTsFileListToSingleMp4 (MergeTsFileListToSingleMp4_Req {
78
205
TsFileList : tsFileList ,
79
- OutputMp4 : OutputMp4Name ,
80
- Ctx : ctx ,
81
- Status : & gMergeStatus ,
206
+ OutputMp4 : tmpOutputName ,
207
+ Ctx : this . ctx ,
208
+ Status : & this . status ,
82
209
})
210
+ this .status .SpeedResetBytes ()
83
211
if err != nil {
84
- resp .ErrMsg = "合并错误: " + err .Error ()
85
- resp .IsCancel = isContextCancel (ctx )
86
- return resp
212
+ this .setErrMsg ("合并错误: " + err .Error ())
213
+ return
87
214
}
88
- return resp
215
+ var contentHash string
216
+ if req .SkipCacheCheck == false {
217
+ this .status .SetProgressBarTitle ("[6/6]计算文件hash" )
218
+ contentHash = getFileSha256 (tmpOutputName )
219
+ if contentHash == "" {
220
+ this .setErrMsg ("无法计算摘要信息: " + tmpOutputName )
221
+ return
222
+ }
223
+ }
224
+ var name string
225
+ for idx := 0 ; ; idx ++ {
226
+ idxS := strconv .Itoa (idx )
227
+ if len (idxS ) < 4 {
228
+ idxS = strings .Repeat ("0" , 4 - len (idxS )) + idxS
229
+ }
230
+ idxS = "_" + idxS
231
+ if idx == 0 {
232
+ name = filepath .Join (req .SaveDir , req .FileName + ".mp4" )
233
+ } else {
234
+ name = filepath .Join (req .SaveDir , req .FileName + idxS + ".mp4" )
235
+ }
236
+ if ! isFileExists (name ) {
237
+ this .setSaveFileTo (name , false )
238
+ break
239
+ }
240
+ if idx > 10000 { // 超过1万就不找了
241
+ this .setErrMsg ("自动寻找文件名失败" )
242
+ return
243
+ }
244
+ }
245
+ err = os .Rename (tmpOutputName , name )
246
+ if err != nil {
247
+ this .setErrMsg ("重命名失败: " + err .Error ())
248
+ return
249
+ }
250
+ if req .SkipCacheCheck == false {
251
+ err = cacheWrite (req .SaveDir , videoId , req , name , contentHash )
252
+ if err != nil {
253
+ this .setErrMsg ("cacheWrite: " + err .Error ())
254
+ return
255
+ }
256
+ }
257
+ if req .SkipRemoveTs == false {
258
+ err = os .RemoveAll (downloadDir )
259
+ if err != nil {
260
+ this .setErrMsg ("删除下载目录失败: " + err .Error ())
261
+ return
262
+ }
263
+ // 如果downloading目录为空,就删除掉,否则忽略
264
+ _ = os .Remove (filepath .Join (req .SaveDir , "downloading" ))
265
+ }
266
+ this .setSaveFileTo (name , false )
267
+ return
89
268
}
90
269
91
- type MergeGetProgressPercent_Resp struct {
92
- Percent int
93
- SpeedText string
94
- IsRunning bool
95
- }
270
+ func (this * DownloadEnv ) setupClient (req StartDownload_Req , proxyUrlObj * url.URL ) {
271
+ if this .nowClient == nil {
272
+ this .nowClient = & http.Client {}
273
+ }
274
+ //关闭以前的空闲链接
275
+ this .nowClient .CloseIdleConnections ()
276
+ this .nowClient .Timeout = time .Second * 20
96
277
97
- func MergeGetProgressPercent () (resp MergeGetProgressPercent_Resp ) {
98
- gMergeIsRunningLocker .Lock ()
99
- resp .IsRunning = gMergeIsRunning
100
- gMergeIsRunningLocker .Unlock ()
101
- if resp .IsRunning {
102
- resp .Percent = gMergeStatus .GetPercent ()
103
- resp .SpeedText = gMergeStatus .SpeedRecent5sGetAndUpdate ()
278
+ if this .nowClient .Transport == nil {
279
+ this .nowClient .Transport = & http.Transport {
280
+ TLSClientConfig : & tls.Config {
281
+ InsecureSkipVerify : req .Insecure ,
282
+ },
283
+ Proxy : http .ProxyURL (proxyUrlObj ),
284
+ }
285
+ } else {
286
+ transport := this .nowClient .Transport .(* http.Transport )
287
+ transport .TLSClientConfig .InsecureSkipVerify = req .Insecure
288
+ transport .Proxy = http .ProxyURL (proxyUrlObj )
104
289
}
105
- return resp
106
290
}
107
291
108
- func MergeStop () {
109
- gMergeCancelLocker .Lock ()
110
- if gMergeCancel != nil {
111
- gMergeCancel ()
292
+ func (this * DownloadEnv ) prepareReqAndHeader (req * StartDownload_Req ) (errMsg string ) {
293
+ if req .SaveDir == "" {
294
+ var err error
295
+ req .SaveDir , err = os .Getwd ()
296
+ if err != nil {
297
+ return "os.Getwd error: " + err .Error ()
298
+ }
299
+ }
300
+ if req .FileName == "" {
301
+ req .FileName = GetFileNameFromUrl (req .M3u8Url )
302
+ }
303
+ if req .SkipTsCountFromHead < 0 {
304
+ req .SkipTsCountFromHead = 0
305
+ }
306
+ host , err := getHost (req .M3u8Url )
307
+ if err != nil {
308
+ return "getHost0: " + err .Error ()
309
+ }
310
+ this .header = http.Header {
311
+ "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" },
312
+ "Connection" : []string {"keep-alive" },
313
+ "Accept" : []string {"*/*" },
314
+ "Accept-Encoding" : []string {"*" },
315
+ "Accept-Language" : []string {"zh-CN,zh;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5" },
316
+ "Referer" : []string {host },
317
+ }
318
+ for key , valueList := range req .HeaderMap {
319
+ this .header [key ] = valueList
112
320
}
113
- gMergeCancelLocker . Unlock ()
321
+ return ""
114
322
}
0 commit comments