├── README.md ├── config.json └── download_geektime_video_.go /README.md: -------------------------------------------------------------------------------- 1 | # download-geektime-video 2 | 此工具为下载极客时间已购课程方便离线观看 3 | 参考:https://github.com/domliang/geektime-dl 4 | 5 | 注意:目前video下载已经失效。 6 | 原因:极客时间用了aliplayer组件并进行了加密,不再直接暴露视频地址。所以暂时还没好办法进行解析。 7 | ### 环境要求 8 | ``` 9 | (1)golang环境 10 | (2)已安装 ffmpeg:brew install ffmpeg 11 | 12 | ``` 13 | ### 使用 14 | 15 | #### 1. clone项目 16 | ``` 17 | git clone https://github.com/wobushixiaoj/download-geektime-video.git 18 | ``` 19 | 20 | #### 2. 进入项目目录,视频下载地址即 download-geektime-video.go 所在的目录 21 | ``` 22 | cd download-geektime-video/ 23 | ``` 24 | 25 | #### 3. 编辑配置文件 26 | ``` 27 | vim config.json 28 | ``` 29 | 配置文件示例 30 | ``` 31 | { 32 | "cid": "98", 33 | "_ga": "GA1.2.1666006123.1560340327", 34 | "_gid": "GA1.2.1666006123.1560340327", 35 | "GCID": "7d63098-852651f-85359ab-348d01c", 36 | "GCESS": "BAYEXcpaOwEEBoYRAAUEAAAAAAIEOI0HXQgBAwsCBAAMAQEKBAAAAAAEBAAvDQAHBOioBL0JAQEDBDiNB10-" 37 | } 38 | ``` 39 | ##### cid获取 40 | 浏览器打开并登录极客时间打开某一课程 41 | ``` 42 | https://time.geekbang.org/course/detail/168-68568 43 | ``` 44 | 其中168是cid 45 | 46 | ##### cookie获取 47 | 拿到 .time.geekbang.org 下面的4个cookie:_ga,_gid,GCID,GCESS 48 | 49 | #### 4.执行下载 50 | ``` 51 | go run download-geektime-video.go 52 | ``` 53 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "cid": "176", 3 | "_ga": "GA1.2.1666006123.1560340327", 4 | "_gid": "GA1.2.1666006123.1560340327", 5 | "GCID": "a63f3db-32b5d17-95bdfb1-423176a", 6 | "GCESS": "BAoEAAAAAAIEfco5XQcEGo8RLgkBAQMEfco5XQwBAQEEBoYRAAYEQygaSQQEAC8NAAgBAwsCBAAFBAAAAAA-", 7 | "article_type": "video" 8 | } -------------------------------------------------------------------------------- /download_geektime_video_.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "math/rand" 9 | "net/http" 10 | "os" 11 | "os/exec" 12 | "strconv" 13 | "strings" 14 | ) 15 | 16 | /** 17 | 用户身份 18 | */ 19 | var _ga string 20 | var _gid string 21 | var GCID string 22 | var GCESS string 23 | 24 | /** 25 | 课程id 26 | */ 27 | var cid string 28 | 29 | var hostname = "https://time.geekbang.org" 30 | var cookie = "_ga=%s; _gid=%s; GCID=%s; GCESS=%s" 31 | var article_type string 32 | 33 | func main() { 34 | 35 | loadConfig() 36 | 37 | articles := getArticles() 38 | 39 | articlesLen := len(articles) 40 | for i := 0; i < articlesLen; i++ { 41 | if article_type == "video" { 42 | downloadVideo(articles[i]) 43 | } else { 44 | downloadText(articles[i]) 45 | } 46 | } 47 | 48 | } 49 | 50 | func getArticles() []Article { 51 | client := http.Client{} 52 | reqBody := fmt.Sprintf("{\"cid\":\"%s\",\"size\":200,\"prev\":0,\"order\":\"earliest\",\"sample\":true}", cid) 53 | 54 | req, _ := http.NewRequest(http.MethodPost, hostname+"/serv/v1/column/articles", strings.NewReader(reqBody)) 55 | 56 | header := req.Header 57 | 58 | buildHeader(header) 59 | 60 | response, _ := client.Do(req) 61 | 62 | dataByte, _ := ioutil.ReadAll(response.Body) 63 | 64 | dataStr := string(dataByte) 65 | if len(dataStr) == 0 { 66 | panic("cookie is not valid! please check your config") 67 | } 68 | 69 | articlesMap := make(map[string]interface{}) 70 | jsonErr := json.Unmarshal(dataByte, &articlesMap) 71 | if jsonErr != nil { 72 | panic(jsonErr) 73 | } 74 | 75 | var videos = articlesMap["data"].(map[string]interface{})["list"].([]interface{}) 76 | 77 | var articles []Article 78 | for _, video := range videos { 79 | var url = "" 80 | if video.(map[string]interface{})["video_media_map"] != nil { 81 | url = video.(map[string]interface{})["video_media_map"].(map[string]interface{})["hd"].(map[string]interface{})["url"].(string) 82 | } 83 | var title = strings.ReplaceAll(strings.ReplaceAll(video.(map[string]interface{})["article_title"].(string), " ", ""), "|", "") 84 | var id = int(video.(map[string]interface{})["id"].(float64)) 85 | article := Article{url, title, id} 86 | articles = append(articles, article) 87 | } 88 | return articles 89 | } 90 | 91 | func buildHeader(header http.Header) { 92 | header.Set("Origin", hostname) 93 | header.Set("Referer", hostname) 94 | header.Set("User-Agent", randomUserAgent()) 95 | header.Set("X-Real-IP", randomIpAddress()) 96 | header.Set("Cookie", fmt.Sprintf(cookie, _ga, _gid, GCID, GCESS)) 97 | header.Set("Connection", "keep-alive") 98 | header.Set("Content-Type", "application/json") 99 | } 100 | 101 | func downloadVideo(article Article) { 102 | path, _ := os.Getwd() 103 | articleSavePath := path + "/" + article.title + ".mp4" 104 | file, _ := os.Stat(articleSavePath) //os.Stat获取文件信息 105 | if file != nil { 106 | fmt.Println("file exists : " + article.title) 107 | return 108 | } 109 | 110 | fmt.Println("start downloadVideo : ", article.title) 111 | cmd := exec.Command("ffmpeg", "-i", article.url, "-c", "copy", "-bsf:a", "aac_adtstoasc", articleSavePath) 112 | 113 | fmt.Println(cmd.Args) 114 | buf := new(bytes.Buffer) 115 | cmd.Stdout = buf 116 | if err := cmd.Run(); err != nil { 117 | os.Remove(articleSavePath) 118 | panic("downloadVideo failed: " + err.Error() + ", " + article.url + ", " + article.title) 119 | } 120 | 121 | fmt.Println("end downloadVideo : ", article.title) 122 | } 123 | 124 | func downloadText(article Article) { 125 | path, _ := os.Getwd() 126 | articleSavePath := path + "/" + article.title + ".html" 127 | file, _ := os.Stat(articleSavePath) //os.Stat获取文件信息 128 | if file != nil { 129 | fmt.Println("file exists : " + article.title) 130 | return 131 | } 132 | 133 | fmt.Println("start downloadText : ", article.title) 134 | 135 | client := http.Client{} 136 | //{"id":"93915","include_neighbors":true} 137 | reqBody := fmt.Sprintf("{\"id\":\"%s\",\"include_neighbors\":true}", strconv.Itoa(article.id)) 138 | req, _ := http.NewRequest(http.MethodPost, hostname+"/serv/v1/article", strings.NewReader(reqBody)) 139 | header := req.Header 140 | buildHeader(header) 141 | response, _ := client.Do(req) 142 | dataByte, _ := ioutil.ReadAll(response.Body) 143 | 144 | dataStr := string(dataByte) 145 | if len(dataStr) == 0 { 146 | panic("cookie is not valid! please check your config") 147 | } 148 | 149 | articlesMap := make(map[string]interface{}) 150 | jsonErr := json.Unmarshal(dataByte, &articlesMap) 151 | if jsonErr != nil { 152 | panic(jsonErr) 153 | } 154 | 155 | var articleContent = articlesMap["data"].(map[string]interface{})["article_content"].(string) 156 | f, err := os.Create(articleSavePath) 157 | if f != nil { 158 | defer f.Close() 159 | if err != nil { 160 | fmt.Println(err.Error()) 161 | } else { 162 | _, err = f.Write([]byte(articleContent)) 163 | } 164 | } 165 | } 166 | 167 | var userAgentList = [13]string{ 168 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36", 169 | "Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1", 170 | "Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1", 171 | "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Mobile Safari/537.36", 172 | "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Mobile Safari/537.36", 173 | "Mozilla/5.0 (Linux; Android 5.1.1; Nexus 6 Build/LYZ28E) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Mobile Safari/537.36", 174 | "Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_2 like Mac OS X) AppleWebKit/603.2.4 (KHTML, like Gecko) Mobile/14F89;GameHelper", 175 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/603.2.4 (KHTML, like Gecko) Version/10.1.1 Safari/603.2.4", 176 | "Mozilla/5.0 (iPhone; CPU iPhone OS 10_0 like Mac OS X) AppleWebKit/602.1.38 (KHTML, like Gecko) Version/10.0 Mobile/14A300 Safari/602.1", 177 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36", 178 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:46.0) Gecko/20100101 Firefox/46.0", 179 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/13.10586", 180 | "Mozilla/5.0 (iPad; CPU OS 10_0 like Mac OS X) AppleWebKit/602.1.38 (KHTML, like Gecko) Version/10.0 Mobile/14A300 Safari/602.1", 181 | } 182 | 183 | func randomUserAgent() string { 184 | r := rand.Intn(13) 185 | return userAgentList[r] 186 | } 187 | 188 | func randomIpAddress() string { 189 | r := rand.Intn(254) 190 | return fmt.Sprintf("211.161.244.%d", r) 191 | } 192 | 193 | func loadConfig() { 194 | f, err := os.Open("./config.json") 195 | 196 | if err != nil { 197 | panic(err) 198 | } 199 | 200 | defer f.Close() 201 | 202 | config := make(map[string]string) 203 | 204 | dataByte, _ := ioutil.ReadAll(f) 205 | 206 | jsonErr := json.Unmarshal(dataByte, &config) 207 | if jsonErr != nil { 208 | panic(jsonErr) 209 | } 210 | 211 | _ga = config["_ga"] 212 | if _ga == "" { 213 | panic("config['_ga'] can not be null") 214 | } 215 | _gid = config["_gid"] 216 | if _gid == "" { 217 | panic("config['_gid'] can not be null") 218 | } 219 | GCID = config["GCID"] 220 | if GCID == "" { 221 | panic("config['GCID'] can not be null") 222 | } 223 | GCESS = config["GCESS"] 224 | if GCESS == "" { 225 | panic("config['GCESS'] can not be null") 226 | } 227 | cid = config["cid"] 228 | if cid == "" { 229 | panic("config['cid'] can not be null") 230 | } 231 | 232 | article_type = config["article_type"] 233 | if article_type == "" { 234 | panic("config['article_type'] can not be null") 235 | } 236 | } 237 | 238 | type Article struct { 239 | url string 240 | title string 241 | id int 242 | } 243 | --------------------------------------------------------------------------------