{"id":15893960,"url":"https://github.com/chinanf-boy/annie-explain","last_synced_at":"2026-03-06T22:05:44.377Z","repository":{"id":90547951,"uuid":"126359430","full_name":"chinanf-boy/annie-explain","owner":"chinanf-boy","description":"explain : 「annie」A fast, simple and clean video downloader  小而简单的视频下载器","archived":false,"fork":false,"pushed_at":"2018-05-08T02:02:14.000Z","size":27,"stargazers_count":5,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-04-20T07:52:10.641Z","etag":null,"topics":["annie","downloader","explain"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/chinanf-boy.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-03-22T15:52:49.000Z","updated_at":"2023-09-07T06:05:05.000Z","dependencies_parsed_at":"2023-07-18T11:00:14.549Z","dependency_job_id":null,"html_url":"https://github.com/chinanf-boy/annie-explain","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/chinanf-boy/annie-explain","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chinanf-boy%2Fannie-explain","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chinanf-boy%2Fannie-explain/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chinanf-boy%2Fannie-explain/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chinanf-boy%2Fannie-explain/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/chinanf-boy","download_url":"https://codeload.github.com/chinanf-boy/annie-explain/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chinanf-boy%2Fannie-explain/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30200756,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-06T19:07:06.838Z","status":"ssl_error","status_checked_at":"2026-03-06T18:57:34.882Z","response_time":250,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["annie","downloader","explain"],"created_at":"2024-10-06T08:14:09.239Z","updated_at":"2026-03-06T22:05:44.342Z","avatar_url":"https://github.com/chinanf-boy.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Annie\n\n「 👾 Annie is a fast, simple and clean video downloader built with Go.\n\n 」\n\n[![explain](http://llever.com/explain.svg)](https://github.com/chinanf-boy/Source-Explain)\n    \nExplanation\n\n\u003e \"version\": \"1.0.0\"\n\n[github source](https://github.com/iawia002/annie)\n\n~~[english](./README.en.md)~~\n\n---\n\n⏬下载啦下载啦\n\n- 本次从小示例出发 比如「抖音」\n\n``` js\n$ annie https://www.douyin.com/share/video/6509219899754155272\n\n Site:    抖音 douyin.com\nTitle:    好冷  逢考必过\n Type:    mp4\n Size:    2.63 MiB (2762719 Bytes)\n\n 741.70 KiB / 2.63 MiB [=========\u003e--------------------------]  27.49% 1.98 MiB/s\n```\n\n\u003e ⏰本项目-`代码示例`运行，请正确-clone本项目到-GOPATH\n\n---\n\n本目录\n\n1. 本章节主要是选择哪个网站的解析器\n\n2. `extractors.explain.md` 是 解析器目录和抖音解析的章节\n\n- [x] [douyin-extractors](#1-extractors-douyin)\n\n- [x] [哔哩哔哩-extractors](./bilibili.extractors.md)\n\n- [ ] 其他\n\n[annie 支持的网站](https://github.com/iawia002/annie#supported-sites)\n\n\n---\n\n## main\n\n\u003e 作为`go`项目，要想作为命令行，被打包📦，被编译，需要具备\n\n``` go\npackage main // 作为 编译入口\n```\n\n`annie/main.go`\n\n``` go\npackage main\n\nimport (\n\t\"flag\" // 作为 go 内置的命令解析\n\t\"fmt\" // 终端-输出\n\t\"net/url\" // 网络/网址\n\n\t\"github.com/iawia002/annie/config\" // 作者-默认配置\n\t\"github.com/iawia002/annie/extractors\" // 作者-对应网站解析\n\t\"github.com/iawia002/annie/utils\" // 作者-工具集\n)\n\nfunc init() { // 初始化 运行顺序派在 func main() 前\n\n// 命令行解析大概描述一下 主要 4个变量\n    // flag.***( 1: 默认配置变量, 2: 命令行选项{-p -i 之类}, 3: 默认值, 4: 说明描述 )\n\tflag.BoolVar(\u0026config.Debug, \"d\", false, \"Debug mode\") // \n\tflag.BoolVar(\u0026config.Version, \"v\", false, \"Show version\")\n\tflag.BoolVar(\u0026config.InfoOnly, \"i\", false, \"Information only\")\n\tflag.StringVar(\u0026config.Cookie, \"c\", \"\", \"Cookie\")\n\tflag.BoolVar(\u0026config.Playlist, \"p\", false, \"Download playlist\")\n\tflag.StringVar(\u0026config.Refer, \"r\", \"\", \"Use specified Referrer\")\n\tflag.StringVar(\u0026config.Proxy, \"x\", \"\", \"HTTP proxy\")\n    flag.StringVar(\u0026config.Socks5Proxy, \"s\", \"\", \"SOCKS5 proxy\")\n\n// 其实 在每个 package 包里面，最先运行的函数都是自定义的 func init()\n}\n\nfunc main() {\n// \u003e 记得我们的小示例吗 `annie https://www.douyin.com/share/video/6509219899754155272` \n\tflag.Parse() // 命令行选项定义后，要开始解析 Parse 启动解析\n\targs := flag.Args() // 除开-定义选项 其他-用户输入命令选项\n\tif config.Version { // 如果 你选项中具备 -v 那么它\n\t\tfmt.Printf(\n\t\t\t\"annie: version %s, A simple and clean video downloader.\\n\", config.VERSION,\n\t\t)\n\t\treturn // 只到这里就结束了\n\t}\n\tif len(args) \u003c 1 { // 没有url-输出错误，但其实应该显示一下粒子\n\t\tfmt.Println(\"error\")\n\t\treturn\n\t}\n\tvideoURL := args[0] // 拿到url\n\tu, err := url.ParseRequestURI(videoURL) // 内置库 url解析\n\tif err != nil {\n\t\tfmt.Println(err)\n\t\treturn\n    }\n    \n\n// 下载抖音的视频\n\n    domain := utils.Domain(u.Host) // 拿到-域\n\n// 在这里-往上⬆️, 做了两件事\n// 1. 控制命令行选项，达到获取配置-config\n// 2. 确认域名\n\n\tswitch domain {\n\tcase \"douyin\":\n\t\textractors.Douyin(videoURL) // 完整网址给到-抖音解析器\n\t// case \"bilibili\":\n\t// \textractors.Bilibili(videoURL)\n    // 。。。\n\n```\n\n- [utils.Domain](#domain)\n\n\u003e 从给予的URL中获得域名\n\n- [config explain ](./config.explain.md)\n\n\u003e 有关-`\"github.com/iawia002/annie/config\"` 的声明与定义\n\n- [extractors explain ](./extractors.explain.md)\n\n\u003e 提取库-定义不同网址的解析\n\n---\n\n## utils\n\n下面工具函数-一般不细看，只要知道什么功能就行\n\n\u003cdetails\u003e\n\n\n``` go\npackage utils\n\nimport (\n\t\"crypto/md5\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"os\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"strings\"\n\n\t\"github.com/iawia002/annie/request\"\n)\n\n```\n\n- [MatchOneOf](#matchoneof)\n\n- [MatchAll](#matchall)\n\n- [FileSize](#filesize)\n\n- [Domain](#domain)\n\n- [FileName](#filename)\n\n- [FilePath](#filepath)\n\n- [StringInSlice](#stringinslice)\n\n- [GetNameAndExt](#getnameandext)\n\n- [Md5](#md5)\n\n- [M3u8URLs](#m3u8urls)\n\n\n``` go\n// MatchOneOf match one of the patterns 匹配一个就返回\nfunc MatchOneOf(text string, patterns ...string) []string {\n\tvar (\n\t\tre    *regexp.Regexp\n\t\tvalue []string\n\t)\n\tfor _, pattern := range patterns {\n\t\tre = regexp.MustCompile(pattern)\n\t\tvalue = re.FindStringSubmatch(text)\n\t\tif len(value) \u003e 0 {\n\t\t\treturn value\n\t\t}\n\t}\n\treturn nil\n}\n\n```\n\n### matchall\n\n``` go\n// MatchAll return all matching results 匹配所有\nfunc MatchAll(text, pattern string) [][]string {\n\tre := regexp.MustCompile(pattern) // 内置\n\tvalue := re.FindAllStringSubmatch(text, -1)\n\treturn value\n}\n\n```\n\n### filesize\n\n``` go\n// FileSize return the file size of the specified path file 返回指定路径文件的文件大小\nfunc FileSize(filePath string) int64 {\n\tfile, err := os.Stat(filePath)\n\tif err != nil \u0026\u0026 os.IsNotExist(err) {\n\t\treturn 0\n\t}\n\treturn file.Size()\n}\n\n```\n\n### domain\n\n``` go\n// Domain get the domain of given URL 从给予的URL中获得域名\nfunc Domain(url string) string {\n\tdomainPattern := `([a-z0-9][-a-z0-9]{0,62})\\.` +\n\t\t`(com\\.cn|com\\.hk|` +\n\t\t`cn|com|net|edu|gov|biz|org|info|pro|name|xxx|xyz|be|` +\n\t\t`me|top|cc|tv|tt)`\n\tdomain := MatchOneOf(url, domainPattern)[1]\n\treturn domain\n}\n\n```\n\n### filename\n\n``` go\n// FileName Converts a string to a valid filename 将字符串转换为有效的文件名\nfunc FileName(name string) string {\n\t// FIXME(iawia002) file name can't have /\n\tname = strings.Replace(name, \"/\", \" \", -1)\n\tname = strings.Replace(name, \"|\", \"-\", -1)\n\tname = strings.Replace(name, \":\", \"：\", -1)\n\tif runtime.GOOS == \"windows\" {\n\t\twinSymbols := []string{\n\t\t\t\"\\\"\", \"?\", \"*\", \"\\\\\", \"\u003c\", \"\u003e\",\n\t\t}\n\t\tfor _, symbol := range winSymbols {\n\t\t\tname = strings.Replace(name, symbol, \" \", -1)\n\t\t}\n\t}\n\treturn name\n}\n\n```\n\n### filepath\n\n``` go\n// FilePath gen valid filename 生成有效的文件名\nfunc FilePath(name, ext string, escape bool) string {\n\tfileName := fmt.Sprintf(\"%s.%s\", name, ext)\n\tif escape {\n\t\tfileName = FileName(fileName)\n\t}\n\treturn fileName\n}\n\n```\n\n### stringinslice\n\n``` go\n// StringInSlice if a string is in the list 如果一个字符串在列表中\nfunc StringInSlice(str string, list []string) bool {\n\tfor _, a := range list {\n\t\tif a == str {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n```\n\n### getnameandext\n\n``` go\n// GetNameAndExt return the name and ext of the URL 返回URL的名称和分机号\n// https://img9.bcyimg.com/drawer/15294/post/1799t/1f5a87801a0711e898b12b640777720f.jpg -\u003e\n// 1f5a87801a0711e898b12b640777720f, jpg\nfunc GetNameAndExt(uri string) (string, string) {\n\tu, _ := url.ParseRequestURI(uri)\n\ts := strings.Split(u.Path, \"/\")\n\tfilename := strings.Split(s[len(s)-1], \".\")\n\tif len(filename) \u003e 1 {\n\t\treturn filename[0], filename[1]\n\t}\n\t// Image url like this\n\t// https://img9.bcyimg.com/drawer/15294/post/1799t/1f5a87801a0711e898b12b640777720f.jpg/w650\n\t// has no suffix\n\tcontentType := request.ContentType(uri, uri)\n\treturn filename[0], strings.Split(contentType, \"/\")[1]\n}\n\n```\n\n### md5\n\n``` go\n// Md5 md5 hash 哈希\nfunc Md5(text string) string {\n\tsign := md5.New()\n\tsign.Write([]byte(text))\n\treturn fmt.Sprintf(\"%x\", sign.Sum(nil))\n}\n\n```\n\n### m3u8urls\n\n``` go\n// M3u8URLs get all urls from m3u8 url 从m3u8网址获取所有网址\nfunc M3u8URLs(uri string) []string {\n\thtml := request.Get(uri)\n\tlines := strings.Split(html, \"\\n\")\n\tvar urls []string\n\tfor _, line := range lines {\n\t\tline = strings.TrimSpace(line)\n\t\tif line != \"\" \u0026\u0026 !strings.HasPrefix(line, \"#\") {\n\t\t\tif strings.HasPrefix(line, \"http\") {\n\t\t\t\turls = append(urls, line)\n\t\t\t} else {\n\t\t\t\tbase, _ := url.Parse(uri)\n\t\t\t\tu, _ := url.Parse(line)\n\t\t\t\turls = append(urls, fmt.Sprintf(\"%s\", base.ResolveReference(u)))\n\t\t\t}\n\t\t}\n\t}\n\treturn urls\n}\n```\n\u003c/details\u003e","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchinanf-boy%2Fannie-explain","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fchinanf-boy%2Fannie-explain","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchinanf-boy%2Fannie-explain/lists"}