├── 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,<title> 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 | --------------------------------------------------------------------------------