├── README.md
├── dowloader.go
├── imgs
└── get-url.png
├── m3u8.go
├── main.go
├── parser.go
└── util
├── crypt.go
├── crypt_test.go
├── http.go
├── http_test.go
├── util.go
└── util_test.go
/README.md:
--------------------------------------------------------------------------------
1 | # xvideos-downloader
2 | xvideos Pornhub 视频下载工具,This Project was fork from [m3u8](https://octolinker-demo.now.sh/oopsguy/m3u8) .
3 |
4 |
5 | # 使用 hls-downloader 获取视频 `m3u8` 下载地址
6 |
7 | ```
8 | https://chrome.google.com/webstore/detail/hls-downloader/apomkbibleomoihlhhdbeghnfioffbej
9 | ```
10 |
11 | * 例如
12 |
13 | [![doodle]][doodle-story]
14 |
15 | [doodle]: ./imgs/get-url.png "从Xvideos取得URL!"
16 | [doodle-story]: https://mls.toh.info/
17 |
18 |
19 | 使用示例:
20 | Windows 下, 下载到当前目录
21 |
22 | ```
23 | .\xvideos-downloader-windows-4.0-386.exe -u "https://hls4-l3.xvideos-cdn.com/42785b68c3681d94b1c140921f90477479a3e64a-1588275324/videos/hls/05/e7/57/05e757806a6ad92386de482f5dd38e02/hls.m3u8" -o .
24 | ```
25 |
26 |
27 | Windows 下, 下载到指定目录
28 |
29 | ```
30 | .\xvideos-downloader-windows-4.0-386.exe -u "https://hls4-l3.xvideos-cdn.com/42785b68c3681d94b1c140921f90477479a3e64a-1588275324/videos/hls/05/e7/57/05e757806a6ad92386de482f5dd38e02/hls.m3u8" -o "D:\"
31 | ```
32 |
33 |
--------------------------------------------------------------------------------
/dowloader.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bufio"
5 | "fmt"
6 | "io/ioutil"
7 | "os"
8 | "path/filepath"
9 | "time"
10 | "math/rand"
11 | "strconv"
12 | "sync"
13 | "sync/atomic"
14 |
15 | "github.com/mask-cx/xvideos-downloader/util"
16 | )
17 |
18 | const (
19 | tsExt = ".ts"
20 | tsFolderName = "tmp-"
21 | tsTempFileSuffix = "_tmp"
22 | progressWidth = 40
23 | letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
24 | )
25 |
26 | func RandStringBytes(n int) string {
27 | rand.Seed(time.Now().UnixNano())
28 | b := make([]byte, n)
29 | for i := range b {
30 | b[i] = letterBytes[rand.Intn(len(letterBytes))]
31 | }
32 | return string(b)
33 | }
34 |
35 | type Downloader struct {
36 | lock sync.Mutex
37 | queue []int
38 | folder string
39 | tsFolder string
40 | finish int32
41 | segLen int
42 |
43 | result *Result
44 | }
45 |
46 | // NewTask returns a Task instance
47 | func NewTask(output string, url string) (*Downloader, error) {
48 | result, err := FromURL(url)
49 | if err != nil {
50 | return nil, err
51 | }
52 | var folder string
53 | // If no output folder specified, use current directory
54 | if output == "" {
55 | current, err := tool.CurrentDir()
56 | if err != nil {
57 | return nil, err
58 | }
59 | folder = filepath.Join(current, output)
60 | } else {
61 | folder = output
62 | }
63 | if err := os.MkdirAll(folder, os.ModePerm); err != nil {
64 | return nil, fmt.Errorf("create storage folder failed: %s", err.Error())
65 | }
66 | tsFolder := filepath.Join(folder, tsFolderName + _taskid )
67 | if err := os.MkdirAll(tsFolder, os.ModePerm); err != nil {
68 | return nil, fmt.Errorf("create ts folder '[%s]' failed: %s", tsFolder, err.Error())
69 | }
70 |
71 | err = ioutil.WriteFile(filepath.Join(folder, _taskid + ".ts.dl"), []byte("Downloading..."), 0755)
72 | if err != nil {
73 | fmt.Printf("Unable to write file: %v", err)
74 | }
75 |
76 | d := &Downloader{
77 | folder: folder,
78 | tsFolder: tsFolder,
79 | result: result,
80 | }
81 | d.segLen = len(result.M3u8.Segments)
82 | d.queue = genSlice(d.segLen)
83 | return d, nil
84 | }
85 |
86 | // Start runs downloader
87 | func (d *Downloader) DownloadStart(concurrency int) error {
88 | var wg sync.WaitGroup
89 | // struct{} zero size
90 | limitChan := make(chan struct{}, concurrency)
91 | for {
92 | tsIdx, end, err := d.next()
93 | if err != nil {
94 | if end {
95 | break
96 | }
97 | continue
98 | }
99 | wg.Add(1)
100 | go func(idx int) {
101 | defer wg.Done()
102 | if err := d.download(idx); err != nil {
103 | // Back into the queue, retry request
104 | fmt.Printf("[failed] %s\n", err.Error())
105 | if err := d.back(idx); err != nil {
106 | fmt.Printf(err.Error())
107 | }
108 | }
109 | <-limitChan
110 | }(tsIdx)
111 | limitChan <- struct{}{}
112 | }
113 | wg.Wait()
114 | if err := d.merge(); err != nil {
115 | return err
116 | }
117 | return nil
118 | }
119 |
120 | func (d *Downloader) download(segIndex int) error {
121 | tsFilename := tsFilename(segIndex)
122 | tsUrl := d.tsURL(segIndex)
123 | b, e := tool.Get(tsUrl)
124 | if e != nil {
125 | return fmt.Errorf("request %s, %s", tsUrl, e.Error())
126 | }
127 | //noinspection GoUnhandledErrorResult
128 | defer b.Close()
129 | fPath := filepath.Join(d.tsFolder, tsFilename)
130 | fTemp := fPath + tsTempFileSuffix
131 | f, err := os.Create(fTemp)
132 | if err != nil {
133 | return fmt.Errorf("create file: %s, %s", tsFilename, err.Error())
134 | }
135 | bytes, err := ioutil.ReadAll(b)
136 | if err != nil {
137 | return fmt.Errorf("read bytes: %s, %s", tsUrl, err.Error())
138 | }
139 | sf := d.result.M3u8.Segments[segIndex]
140 | if sf == nil {
141 | return fmt.Errorf("invalid segment index: %d", segIndex)
142 | }
143 | key, ok := d.result.Keys[sf.KeyIndex]
144 | if ok && key != "" {
145 | bytes, err = tool.AES128Decrypt(bytes, []byte(key),
146 | []byte(d.result.M3u8.Keys[sf.KeyIndex].IV))
147 | if err != nil {
148 | return fmt.Errorf("decryt: %s, %s", tsUrl, err.Error())
149 | }
150 | }
151 | // https://en.wikipedia.org/wiki/MPEG_transport_stream
152 | // Some TS files do not start with SyncByte 0x47, they can not be played after merging,
153 | // Need to remove the bytes before the SyncByte 0x47(71).
154 | syncByte := uint8(71) //0x47
155 | bLen := len(bytes)
156 | for j := 0; j < bLen; j++ {
157 | if bytes[j] == syncByte {
158 | bytes = bytes[j:]
159 | break
160 | }
161 | }
162 | w := bufio.NewWriter(f)
163 | if _, err := w.Write(bytes); err != nil {
164 | return fmt.Errorf("write to %s: %s", fTemp, err.Error())
165 | }
166 | // Release file resource to rename file
167 | _ = f.Close()
168 | if err = os.Rename(fTemp, fPath); err != nil {
169 | return err
170 | }
171 | // Maybe it will be safer in this way...
172 | atomic.AddInt32(&d.finish, 1)
173 | //tool.DrawProgressBar("Downloading", float32(d.finish)/float32(d.segLen), progressWidth)
174 | fmt.Printf("[download %6.2f%%] %s\n", float32(d.finish)/float32(d.segLen)*100, tsUrl)
175 | return nil
176 | }
177 |
178 | func (d *Downloader) next() (segIndex int, end bool, err error) {
179 | d.lock.Lock()
180 | defer d.lock.Unlock()
181 | if len(d.queue) == 0 {
182 | err = fmt.Errorf("queue empty")
183 | if d.finish == int32(d.segLen) {
184 | end = true
185 | return
186 | }
187 | // Some segment indexes are still running.
188 | end = false
189 | return
190 | }
191 | segIndex = d.queue[0]
192 | d.queue = d.queue[1:]
193 | return
194 | }
195 |
196 | func (d *Downloader) back(segIndex int) error {
197 | d.lock.Lock()
198 | defer d.lock.Unlock()
199 | if sf := d.result.M3u8.Segments[segIndex]; sf == nil {
200 | return fmt.Errorf("invalid segment index: %d", segIndex)
201 | }
202 | d.queue = append(d.queue, segIndex)
203 | return nil
204 | }
205 |
206 | func (d *Downloader) merge() error {
207 | // In fact, the number of downloaded segments should be equal to number of m3u8 segments
208 | missingCount := 0
209 | for idx := 0; idx < d.segLen; idx++ {
210 | tsFilename := tsFilename(idx)
211 | f := filepath.Join(d.tsFolder, tsFilename)
212 | if _, err := os.Stat(f); err != nil {
213 | missingCount++
214 | }
215 | }
216 | if missingCount > 0 {
217 | fmt.Printf("[warning] %d files missing\n", missingCount)
218 | }
219 |
220 | mergeTSFilename := _taskid + ".ts"
221 | // Create a TS file for merging, all segment files will be written to this file.
222 | mFilePath := filepath.Join(d.folder, mergeTSFilename)
223 | os.Remove( mFilePath + ".dl")
224 | mFile, err := os.Create(mFilePath)
225 | if err != nil {
226 | return fmt.Errorf("create main TS file failed:%s", err.Error())
227 | }
228 | //noinspection GoUnhandledErrorResult
229 | defer mFile.Close()
230 |
231 | writer := bufio.NewWriter(mFile)
232 | mergedCount := 0
233 | for segIndex := 0; segIndex < d.segLen; segIndex++ {
234 | tsFilename := tsFilename(segIndex)
235 | bytes, err := ioutil.ReadFile(filepath.Join(d.tsFolder, tsFilename))
236 | _, err = writer.Write(bytes)
237 | if err != nil {
238 | continue
239 | }
240 | mergedCount++
241 | tool.DrawProgressBar("merge",
242 | float32(mergedCount)/float32(d.segLen), progressWidth)
243 | }
244 | _ = writer.Flush()
245 | // Remove `ts` folder
246 | _ = os.RemoveAll(d.tsFolder)
247 |
248 | if mergedCount != d.segLen {
249 | fmt.Printf("[warning] \n%d files merge failed", d.segLen-mergedCount)
250 | }
251 |
252 | fmt.Printf("\n[output] %s\n", mFilePath)
253 |
254 | return nil
255 | }
256 |
257 | func (d *Downloader) tsURL(segIndex int) string {
258 | seg := d.result.M3u8.Segments[segIndex]
259 | return tool.ResolveURL(d.result.URL, seg.URI)
260 | }
261 |
262 | func tsFilename(ts int) string {
263 | return strconv.Itoa(ts) + tsExt
264 | }
265 |
266 | func genSlice(len int) []int {
267 | s := make([]int, 0)
268 | for i := 0; i < len; i++ {
269 | s = append(s, i)
270 | }
271 | return s
272 | }
273 |
--------------------------------------------------------------------------------
/imgs/get-url.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mask-cx/xvideos-downloader/b241d088cb8e3041fa73f28f7b1b806eadcfae26/imgs/get-url.png
--------------------------------------------------------------------------------
/m3u8.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bufio"
5 | "errors"
6 | "fmt"
7 | "io"
8 | "regexp"
9 | "strconv"
10 | "strings"
11 | )
12 |
13 | type (
14 | PlaylistType string
15 | CryptMethod string
16 | )
17 |
18 | const (
19 | PlaylistTypeVOD PlaylistType = "VOD"
20 | PlaylistTypeEvent PlaylistType = "EVENT"
21 |
22 | CryptMethodAES CryptMethod = "AES-128"
23 | CryptMethodNONE CryptMethod = "NONE"
24 | )
25 |
26 | // regex pattern for extracting `key=value` parameters from a line
27 | var linePattern = regexp.MustCompile(`([a-zA-Z-]+)=("[^"]+"|[^",]+)`)
28 |
29 | type M3u8 struct {
30 | Version int8 // EXT-X-VERSION:version
31 | MediaSequence uint64 // Default 0, #EXT-X-MEDIA-SEQUENCE:sequence
32 | Segments []*Segment
33 | MasterPlaylist []*MasterPlaylist
34 | Keys map[int]*Key
35 | EndList bool // #EXT-X-ENDLIST
36 | PlaylistType PlaylistType // VOD or EVENT
37 | TargetDuration float64 // #EXT-X-TARGETDURATION:duration
38 | }
39 |
40 | type Segment struct {
41 | URI string
42 | KeyIndex int
43 | Title string // #EXTINF: duration,
44 | Duration float32 // #EXTINF: duration,
45 | Length uint64 // #EXT-X-BYTERANGE: length[@offset]
46 | Offset uint64 // #EXT-X-BYTERANGE: length[@offset]
47 | }
48 |
49 | // #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=240000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2"
50 | type MasterPlaylist struct {
51 | URI string
52 | BandWidth uint32
53 | Resolution string
54 | Codecs string
55 | ProgramID uint32
56 | }
57 |
58 | // #EXT-X-KEY:METHOD=AES-128,URI="key.key"
59 | type Key struct {
60 | // 'AES-128' or 'NONE'
61 | // If the encryption method is NONE, the URI and the IV attributes MUST NOT be present
62 | Method CryptMethod
63 | URI string
64 | IV string
65 | }
66 |
67 | func parse(reader io.Reader) (*M3u8, error) {
68 | s := bufio.NewScanner(reader)
69 | var lines []string
70 | for s.Scan() {
71 | lines = append(lines, s.Text())
72 | }
73 |
74 | var (
75 | i = 0
76 | count = len(lines)
77 | m3u8 = &M3u8{
78 | Keys: make(map[int]*Key),
79 | }
80 | keyIndex = 0
81 |
82 | key *Key
83 | seg *Segment
84 | extInf bool
85 | extByte bool
86 | )
87 |
88 | for ; i < count; i++ {
89 | line := strings.TrimSpace(lines[i])
90 | if i == 0 {
91 | if "#EXTM3U" != line {
92 | return nil, fmt.Errorf("invalid m3u8, missing #EXTM3U in line 1")
93 | }
94 | continue
95 | }
96 | switch {
97 | case line == "":
98 | continue
99 | case strings.HasPrefix(line, "#EXT-X-PLAYLIST-TYPE:"):
100 | if _, err := fmt.Sscanf(line, "#EXT-X-PLAYLIST-TYPE:%s", &m3u8.PlaylistType); err != nil {
101 | return nil, err
102 | }
103 | isValid := m3u8.PlaylistType == "" || m3u8.PlaylistType == PlaylistTypeVOD || m3u8.PlaylistType == PlaylistTypeEvent
104 | if !isValid {
105 | return nil, fmt.Errorf("invalid playlist type: %s, line: %d", m3u8.PlaylistType, i+1)
106 | }
107 | case strings.HasPrefix(line, "#EXT-X-TARGETDURATION:"):
108 | if _, err := fmt.Sscanf(line, "#EXT-X-TARGETDURATION:%f", &m3u8.TargetDuration); err != nil {
109 | return nil, err
110 | }
111 | case strings.HasPrefix(line, "#EXT-X-MEDIA-SEQUENCE:"):
112 | if _, err := fmt.Sscanf(line, "#EXT-X-MEDIA-SEQUENCE:%d", &m3u8.MediaSequence); err != nil {
113 | return nil, err
114 | }
115 | case strings.HasPrefix(line, "#EXT-X-VERSION:"):
116 | if _, err := fmt.Sscanf(line, "#EXT-X-VERSION:%d", &m3u8.Version); err != nil {
117 | return nil, err
118 | }
119 | // Parse master playlist
120 | case strings.HasPrefix(line, "#EXT-X-STREAM-INF:"):
121 | mp, err := parseMasterPlaylist(line)
122 | if err != nil {
123 | return nil, err
124 | }
125 | i++
126 | mp.URI = lines[i]
127 | if mp.URI == "" || strings.HasPrefix(mp.URI, "#") {
128 | return nil, fmt.Errorf("invalid EXT-X-STREAM-INF URI, line: %d", i+1)
129 | }
130 | m3u8.MasterPlaylist = append(m3u8.MasterPlaylist, mp)
131 | continue
132 | case strings.HasPrefix(line, "#EXTINF:"):
133 | if extInf {
134 | return nil, fmt.Errorf("duplicate EXTINF: %s, line: %d", line, i+1)
135 | }
136 | if seg == nil {
137 | seg = new(Segment)
138 | }
139 | var s string
140 | if _, err := fmt.Sscanf(line, "#EXTINF:%s", &s); err != nil {
141 | return nil, err
142 | }
143 | if strings.Contains(s, ",") {
144 | split := strings.Split(s, ",")
145 | seg.Title = split[1]
146 | s = split[0]
147 | }
148 | df, err := strconv.ParseFloat(s, 32)
149 | if err != nil {
150 | return nil, err
151 | }
152 | seg.Duration = float32(df)
153 | seg.KeyIndex = keyIndex
154 | extInf = true
155 | case strings.HasPrefix(line, "#EXT-X-BYTERANGE:"):
156 | if extByte {
157 | return nil, fmt.Errorf("duplicate EXT-X-BYTERANGE: %s, line: %d", line, i+1)
158 | }
159 | if seg == nil {
160 | seg = new(Segment)
161 | }
162 | var b string
163 | if _, err := fmt.Sscanf(line, "#EXT-X-BYTERANGE:%s", &b); err != nil {
164 | return nil, err
165 | }
166 | if b == "" {
167 | return nil, fmt.Errorf("invalid EXT-X-BYTERANGE, line: %d", i+1)
168 | }
169 | if strings.Contains(b, "@") {
170 | split := strings.Split(b, "@")
171 | offset, err := strconv.ParseUint(split[1], 10, 64)
172 | if err != nil {
173 | return nil, err
174 | }
175 | seg.Offset = uint64(offset)
176 | b = split[0]
177 | }
178 | length, err := strconv.ParseUint(b, 10, 64)
179 | if err != nil {
180 | return nil, err
181 | }
182 | seg.Length = uint64(length)
183 | extByte = true
184 | // Parse segments URI
185 | case !strings.HasPrefix(line, "#"):
186 | if extInf {
187 | if seg == nil {
188 | return nil, fmt.Errorf("invalid line: %s", line)
189 | }
190 | seg.URI = line
191 | extByte = false
192 | extInf = false
193 | m3u8.Segments = append(m3u8.Segments, seg)
194 | seg = nil
195 | continue
196 | }
197 | // Parse key
198 | case strings.HasPrefix(line, "#EXT-X-KEY"):
199 | params := parseLineParameters(line)
200 | if len(params) == 0 {
201 | return nil, fmt.Errorf("invalid EXT-X-KEY: %s, line: %d", line, i+1)
202 | }
203 | method := CryptMethod(params["METHOD"])
204 | if method != "" && method != CryptMethodAES && method != CryptMethodNONE {
205 | return nil, fmt.Errorf("invalid EXT-X-KEY method: %s, line: %d", method, i+1)
206 | }
207 | keyIndex++
208 | key = new(Key)
209 | key.Method = method
210 | key.URI = params["URI"]
211 | key.IV = params["IV"]
212 | m3u8.Keys[keyIndex] = key
213 | case line == "#EndList":
214 | m3u8.EndList = true
215 | default:
216 | continue
217 | }
218 | }
219 |
220 | return m3u8, nil
221 | }
222 |
223 | func parseMasterPlaylist(line string) (*MasterPlaylist, error) {
224 | params := parseLineParameters(line)
225 | if len(params) == 0 {
226 | return nil, errors.New("empty parameter")
227 | }
228 | mp := new(MasterPlaylist)
229 | for k, v := range params {
230 | switch {
231 | case k == "BANDWIDTH":
232 | v, err := strconv.ParseUint(v, 10, 32)
233 | if err != nil {
234 | return nil, err
235 | }
236 | mp.BandWidth = uint32(v)
237 | case k == "RESOLUTION":
238 | mp.Resolution = v
239 | case k == "PROGRAM-ID":
240 | v, err := strconv.ParseUint(v, 10, 32)
241 | if err != nil {
242 | return nil, err
243 | }
244 | mp.ProgramID = uint32(v)
245 | case k == "CODECS":
246 | mp.Codecs = v
247 | }
248 | }
249 | return mp, nil
250 | }
251 |
252 | // parseLineParameters extra parameters in string `line`
253 | func parseLineParameters(line string) map[string]string {
254 | r := linePattern.FindAllStringSubmatch(line, -1)
255 | params := make(map[string]string)
256 | for _, arr := range r {
257 | params[arr[1]] = strings.Trim(arr[2], "\"")
258 | }
259 | return params
260 | }
261 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "os"
7 |
8 | )
9 |
10 | var (
11 | _taskid string
12 | _url string
13 | output string
14 | chanSize int
15 | )
16 |
17 | func init() {
18 | flag.StringVar(&_url, "u", "", "M3U8 url")
19 | flag.IntVar(&chanSize, "c", 25, "Maximum number of occurrences")
20 | flag.StringVar(&output, "o", "", "Save files to PREFIX/.. Output to folder")
21 | }
22 |
23 | func main() {
24 | flag.Parse()
25 | defer func() {
26 | if r := recover(); r != nil {
27 | fmt.Println("[error]", r)
28 | os.Exit(-1)
29 | }
30 | }()
31 | if _url == "" {
32 | fatal("u")
33 | }
34 | if output == "" {
35 | fatal("o")
36 | }
37 | if chanSize <= 0 {
38 | panic(" '-c' must bigger than 0")
39 | }
40 | _taskid = RandStringBytes(8)
41 | downloader, err := NewTask(output, _url)
42 | if err != nil {
43 | panic(err)
44 | }
45 | if err := downloader.DownloadStart(chanSize); err != nil {
46 | panic(err)
47 | }
48 | fmt.Println("Done!")
49 | }
50 |
51 | func fatal(name string) {
52 | panic(" '-" + name + "' is required")
53 | }
54 |
--------------------------------------------------------------------------------
/parser.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "io/ioutil"
7 | "net/url"
8 |
9 | "github.com/mask-cx/xvideos-downloader/util"
10 | )
11 |
12 | type Result struct {
13 | URL *url.URL
14 | M3u8 *M3u8
15 | Keys map[int]string
16 | }
17 |
18 | func FromURL(link string) (*Result, error) {
19 | u, err := url.Parse(link)
20 | if err != nil {
21 | return nil, err
22 | }
23 | link = u.String()
24 | body, err := tool.Get(link)
25 | if err != nil {
26 | return nil, fmt.Errorf("request m3u8 URL failed: %s", err.Error())
27 | }
28 | //noinspection GoUnhandledErrorResult
29 | defer body.Close()
30 | m3u8, err := parse(body)
31 | if err != nil {
32 | return nil, err
33 | }
34 | if len(m3u8.MasterPlaylist) != 0 {
35 | sf := m3u8.MasterPlaylist[0]
36 | return FromURL(tool.ResolveURL(u, sf.URI))
37 | }
38 | if len(m3u8.Segments) == 0 {
39 | return nil, errors.New("can not found any TS file description")
40 | }
41 | result := &Result{
42 | URL: u,
43 | M3u8: m3u8,
44 | Keys: make(map[int]string),
45 | }
46 |
47 | for idx, key := range m3u8.Keys {
48 | switch {
49 | case key.Method == "" || key.Method == CryptMethodNONE:
50 | continue
51 | case key.Method == CryptMethodAES:
52 | // Request URL to extract decryption key
53 | keyURL := key.URI
54 | keyURL = tool.ResolveURL(u, keyURL)
55 | resp, err := tool.Get(keyURL)
56 | if err != nil {
57 | return nil, fmt.Errorf("extract key failed: %s", err.Error())
58 | }
59 | keyByte, err := ioutil.ReadAll(resp)
60 | _ = resp.Close()
61 | if err != nil {
62 | return nil, err
63 | }
64 | fmt.Println("decryption key: ", string(keyByte))
65 | result.Keys[idx] = string(keyByte)
66 | default:
67 | return nil, fmt.Errorf("unknown or unsupported cryption method: %s", key.Method)
68 | }
69 | }
70 | return result, nil
71 | }
72 |
--------------------------------------------------------------------------------
/util/crypt.go:
--------------------------------------------------------------------------------
1 | package tool
2 |
3 | import (
4 | "bytes"
5 | "crypto/aes"
6 | "crypto/cipher"
7 | )
8 |
9 | func AES128Encrypt(origData, key, iv []byte) ([]byte, error) {
10 | block, err := aes.NewCipher(key)
11 | if err != nil {
12 | return nil, err
13 | }
14 | blockSize := block.BlockSize()
15 | if len(iv) == 0 {
16 | iv = key
17 | }
18 | origData = pkcs5Padding(origData, blockSize)
19 | blockMode := cipher.NewCBCEncrypter(block, iv[:blockSize])
20 | crypted := make([]byte, len(origData))
21 | blockMode.CryptBlocks(crypted, origData)
22 | return crypted, nil
23 | }
24 |
25 | func AES128Decrypt(crypted, key, iv []byte) ([]byte, error) {
26 | block, err := aes.NewCipher(key)
27 | if err != nil {
28 | return nil, err
29 | }
30 | blockSize := block.BlockSize()
31 | if len(iv) == 0 {
32 | iv = key
33 | }
34 | blockMode := cipher.NewCBCDecrypter(block, iv[:blockSize])
35 | origData := make([]byte, len(crypted))
36 | blockMode.CryptBlocks(origData, crypted)
37 | origData = pkcs5UnPadding(origData)
38 | return origData, nil
39 | }
40 |
41 | func pkcs5Padding(cipherText []byte, blockSize int) []byte {
42 | padding := blockSize - len(cipherText)%blockSize
43 | padText := bytes.Repeat([]byte{byte(padding)}, padding)
44 | return append(cipherText, padText...)
45 | }
46 |
47 | func pkcs5UnPadding(origData []byte) []byte {
48 | length := len(origData)
49 | unPadding := int(origData[length-1])
50 | return origData[:(length - unPadding)]
51 | }
52 |
--------------------------------------------------------------------------------
/util/crypt_test.go:
--------------------------------------------------------------------------------
1 | package tool
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func Test_AES128Encrypt_AND_AES128Decrypt(t *testing.T) {
8 | expected := "helloworld"
9 | key := "8dv4byf8b9e6bc1x"
10 | iv := "xduio1f8a12348u4"
11 | encrypt, err := AES128Encrypt([]byte(expected), []byte(key), []byte(iv))
12 | if err != nil {
13 | t.Fatal(err)
14 | }
15 | decrypt, err := AES128Decrypt(encrypt, []byte(key), []byte(iv))
16 | if err != nil {
17 | t.Fatal(err)
18 | }
19 | de := string(decrypt)
20 | if de != expected {
21 | t.Fatalf("expected: %s, result: %s", expected, de)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/util/http.go:
--------------------------------------------------------------------------------
1 | package tool
2 |
3 | import (
4 | "fmt"
5 | "io"
6 | "net/http"
7 | "time"
8 | )
9 |
10 | func Get(url string) (io.ReadCloser, error) {
11 | c := http.Client{
12 | Timeout: time.Duration(60) * time.Second,
13 | }
14 | resp, err := c.Get(url)
15 | if err != nil {
16 | return nil, err
17 | }
18 | if resp.StatusCode != 200 {
19 | return nil, fmt.Errorf("http error: status code %d", resp.StatusCode)
20 | }
21 | return resp.Body, nil
22 | }
23 |
--------------------------------------------------------------------------------
/util/http_test.go:
--------------------------------------------------------------------------------
1 | package tool
2 |
3 | import (
4 | "io/ioutil"
5 | "testing"
6 | )
7 |
8 | func TestGet(t *testing.T) {
9 | body, err := Get("https://raw.githubusercontent.com/oopsguy/m3u8/master/README.md")
10 | if err != nil {
11 | t.Error(err)
12 | }
13 | defer body.Close()
14 | _, err = ioutil.ReadAll(body)
15 | if err != nil {
16 | t.Error(err)
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/util/util.go:
--------------------------------------------------------------------------------
1 | package tool
2 |
3 | import (
4 | "fmt"
5 | "net/url"
6 | "os"
7 | "path"
8 | "path/filepath"
9 | "strings"
10 | )
11 |
12 | func CurrentDir(joinPath ...string) (string, error) {
13 | dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
14 | if err != nil {
15 | return "", err
16 | }
17 | p := strings.Replace(dir, "\\", "/", -1)
18 | whole := filepath.Join(joinPath...)
19 | whole = filepath.Join(p, whole)
20 | return whole, nil
21 | }
22 |
23 | func ResolveURL(u *url.URL, p string) string {
24 | if strings.HasPrefix(p, "https://") || strings.HasPrefix(p, "http://") {
25 | return p
26 | }
27 | var baseURL string
28 | if strings.Index(p, "/") == 0 {
29 | baseURL = u.Scheme + "://" + u.Host
30 | } else {
31 | tU := u.String()
32 | baseURL = tU[0:strings.LastIndex(tU, "/")]
33 | }
34 | return baseURL + path.Join("/", p)
35 | }
36 |
37 | func DrawProgressBar(prefix string, proportion float32, width int, suffix ...string) {
38 | pos := int(proportion * float32(width))
39 | s := fmt.Sprintf("[%s] %s%*s %6.2f%% %s",
40 | prefix, strings.Repeat("■", pos), width-pos, "", proportion*100, strings.Join(suffix, ""))
41 | fmt.Print("\r" + s)
42 | }
43 |
--------------------------------------------------------------------------------
/util/util_test.go:
--------------------------------------------------------------------------------
1 | package tool
2 |
3 | import (
4 | "net/url"
5 | "testing"
6 | )
7 |
8 | func TestResolveURL(t *testing.T) {
9 | testURL := "http://www.example.com/test/index.m3m8"
10 | u, err := url.Parse(testURL)
11 | if err != nil {
12 | t.Error(err)
13 | }
14 |
15 | result := ResolveURL(u, "videos/111111.ts")
16 | expected := "http://www.example.com/test/videos/111111.ts"
17 | if result != expected {
18 | t.Fatalf("wrong URL, expected: %s, result: %s", expected, result)
19 | }
20 |
21 | result = ResolveURL(u, "/videos/2222222.ts")
22 | expected = "http://www.example.com/videos/2222222.ts"
23 | if result != expected {
24 | t.Fatalf("wrong URL, expected: %s, result: %s", expected, result)
25 | }
26 |
27 | result = ResolveURL(u, "https://test.com/11111.key")
28 | expected = "https://test.com/11111.key"
29 | if result != expected {
30 | t.Fatalf("wrong URL, expected: %s, result: %s", expected, result)
31 | }
32 | }
33 |
--------------------------------------------------------------------------------