diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml new file mode 100644 index 0000000..fa471c0 --- /dev/null +++ b/.github/workflows/build-and-test.yml @@ -0,0 +1,47 @@ +name: Build & Test +on: + push: + branches: + - master + pull_request: + branches: + - master + workflow_dispatch: +jobs: + ci: + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + strategy: + matrix: + go: [1.17.x] + os: [ubuntu-latest] + name: Go ${{ matrix.go }} in ${{ matrix.os }} + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go }} + - name: Environment + run: | + go version + go env + - name: Cache go module + uses: actions/cache@v2 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: ${{ runner.os }}-go- + - name: Install tools + env: + GO111MODULE: on + run: | + go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.42.1 + golangci-lint --version + - name: Lint + run: | + go mod tidy + golangci-lint run ./... -v + - name: Test + env: + GOFLAGS: -mod=mod + run: go test -race -coverpkg=./... -coverprofile=coverage.txt ./... diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..9be3b65 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,87 @@ +name: Release + +on: + push: + tags: + - "v*" + workflow_dispatch: +# inputs: +# upx_version: +# description: 'UPX version' +# required: true +# default: '3.95' + +jobs: + release: + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + strategy: + matrix: + go: [1.17.x] + os: [ubuntu-latest] + name: Go ${{ matrix.go }} in ${{ matrix.os }} + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go }} + - name: Environment + run: | + go version + go env + - name: Install tools + env: + GO111MODULE: on + run: | + go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.42.1 + golangci-lint --version + - name: Lint + run: | + go mod tidy + golangci-lint run ./... -v + - name: Test + env: + GOFLAGS: -mod=mod + run: go test -race -coverpkg=./... -coverprofile=coverage.txt ./... + + # So GoReleaser can generate the changelog properly + - name: Unshallowify the repo clone + run: git fetch --prune --unshallow + + - name: Check git tag exist + run: | + git fetch --depth=1 origin +refs/tags/*:refs/tags/* + gitTagExists=$(git tag --points-at HEAD) + if ! [ -n "$gitTagExists" ]; then + echo "no tag, create one." + latesttag=$(git describe --tags `git rev-list --tags --max-count=1`) + echo "latest tag: ${latesttag}" + newtag=$(echo ${latesttag} | awk -F. -v OFS=. 'NF==1{print ++$NF}; NF>1{if(length($NF+1)>length($NF))$(NF-1)++; $NF=sprintf("%0*d", length($NF), ($NF+1)%(10^length($NF))); print}') + echo "new tag: ${newtag}" + git tag $newtag + fi + +# - name: Installing upx +# env: +# UPX_VERSION: ${{ github.event.inputs.upx_version }} +# run: | +# echo "Installing upx .." +# curl -OL "https://github.com/upx/upx/releases/download/v${UPX_VERSION}/upx-${UPX_VERSION}-amd64_linux.tar.xz" +# tar xvf "upx-${UPX_VERSION}-amd64_linux.tar.xz" +# cp "upx-${UPX_VERSION}-amd64_linux/upx" "$(go env GOPATH)/bin" +# upx --version + + - name: Set environment variables + run: | + echo "GOLANG_VERSION=$(go version)" >> $GITHUB_ENV + echo "BUILT_BY=$(whoami)@$(hostname)" >> $GITHUB_ENV + + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v2 + with: + version: latest + args: release --rm-dist --skip-validate --skip-sign --debug + env: + GITHUB_TOKEN: ${{ secrets.PAT }} + GOLANG_VERSION: ${{ env.GOLANG_VERSION }} + BUILT_BY: ${{ env.BUILT_BY }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..28b009f --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ +m3u8-downloader +.vscode +/outputFolder \ No newline at end of file diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..4b455f5 --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,58 @@ +env: + - GO111MODULE=on + +builds: + - env: + - CGO_ENABLED=0 + ldflags: + - '-s -w' + - -X 'github.com/{{.Env.GITHUB_REPOSITORY}}/cmd.Version={{.Version}}' + - -X 'github.com/{{.Env.GITHUB_REPOSITORY}}/cmd.Commit={{.FullCommit}}' + - -X 'github.com/{{.Env.GITHUB_REPOSITORY}}/cmd.RepoUrl={{.GitURL}}' + - -X 'github.com/{{.Env.GITHUB_REPOSITORY}}/cmd.BuildDate={{.Date}}' + - -X 'github.com/{{.Env.GITHUB_REPOSITORY}}/cmd.BuiltWithGoVersion={{.Env.GOLANG_VERSION}}' + - -X 'github.com/{{.Env.GITHUB_REPOSITORY}}/cmd.BuiltBy={{.Env.BUILT_BY}}' + goos: + - windows + - darwin + - linux + - freebsd + goarch: + - 386 + - amd64 + - arm + - arm64 + - mips + - mipsle + - mips64 + - mips64le + gomips: + - hardfloat + - softfloat +# hooks: +# post: +# - bash -c 'upx "{{ .Path }}" || true' +archives: + - name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}_v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}' + format: tar.gz + files: + - none* + format_overrides: + - goos: windows + format: zip + wrap_in_directory: false + replacements: + amd64: 64-bit + 386: 32-bit + arm: ARM + arm64: ARM64 + darwin: macOS + linux: Linux + windows: Windows + openbsd: OpenBSD + netbsd: NetBSD + freebsd: FreeBSD +release: + draft: true + prerelease: auto + name_template: '{{.ProjectName}}-v{{.Version}}-{{.Date}}' diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..60503fe --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Char + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..5368e14 --- /dev/null +++ b/README.md @@ -0,0 +1,155 @@ +# m3u8-downloader + +[![Build Status](https://github.com/cxjava/m3u8-downloader/actions/workflows/build-and-test.yml/badge.svg)](https://github.com/cxjava/m3u8-downloader/actions/workflows/build-and-test.yml) +[![License](https://img.shields.io/github/license/cxjava/m3u8-downloader.svg)](https://github.com/cxjava/m3u8-downloader) +[![Release](https://img.shields.io/github/release/cxjava/m3u8-downloader.svg)](https://github.com/cxjava/m3u8-downloader/releases) +![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/cxjava/m3u8-downloader) + +README | [简体中文](README.zh-cn.md) + +m3u8 downloader by Golang + +![m3u8-downloader-ScreenShot](https://user-images.githubusercontent.com/802316/133533481-483aa464-2fbe-4a25-9539-4a6345481dcd.png) + +## feature + +- Support CDN download, break the speed limit +- Support custom key, the format of the key can be hex or base64, or original +- Support custom host prefix +- Read m3u8 from file or m3u8 from network address +- Support proxy +- Support custom request header +- Whether to delete the downloaded ts file +- Custom number of threads +- Custom download file name +- Progress bar +- Support running on Apple M1, router + +## TODO + +- [ ] Support decryption while downloading +- [ ] ffmpeg processing command +- [ ] Show total file size +- [ ] Delete error CDN IP + +## Ping command + +``` shell +./m3u8-downloader p -h + +With this command, you can ping multiple IPs to detect IP response speeds. Easily get the fastest CDN IPs.IP is separated by commas, for example: ./m3u8-downloader p 8.8.8.8,1.1.1.1,9.9.9.9, The have two output format, default output is speed of each IP, for example: + +./m3u8-downloader p 8.8.8.8,1.1.1.1,9.9.9.9, + +9.9.9.9 time: 23.9635ms +8.8.8.8 time: 61.2325ms +1.1.1.1 time: 225.7115ms + +Other output is the parameter of download m3u8, for example: + +./m3u8-downloader p 8.8.8.8,1.1.1.1,9.9.9.9, -d www.google.com -o p + +-C 'www.google.com:8.8.8.8' -C 'www.google.com:1.1.1.1' -C 'www.google.com:9.9.9.9' + +Usage: + m3u8-downloader ping [flags] + +Aliases: + ping, p + +Flags: + -d, --domain string Only avaliable when output type is 'p', domain to use output as a parameter, such as: -C 'www.google.com:1.1.1.1' -C 'www.google.com:8.8.8.8' (default "www.google.com") + -h, --help help for ping + -l, --logLevel string logging level on a Logger,logging levels: Trace, Debug, Info, Warning, Error, Fatal and Panic. (default "Info") + -o, --outputType string output type, can be 't' or 'p', if the type is 'p', it will print "-C 'www.example.com:1.1.1.1' -C 'www.example.com:9.9.9.9'". If the type is 't', it will print 8.8.8.8 time: 62.17ms (default "t") + +Global Flags: + --config string config file (default is $HOME/.m3u8-downloader.yaml) +``` + +### How to use CDN for download + +- Open [https://ping.chinaz.com/](https://ping.chinaz.com/) Enter the address of the domain name for which you need to find the IP, and click the Copy button after the ping is finished + +![1](https://user-images.githubusercontent.com/802316/133531905-ac398cc4-77da-44e3-a309-351feebd0628.png) + +- Run the `ping` command to query the ping time of each IP + +``` shell +./m3u8-downloader p 8.8.8.8,1.1.1.1,9.9.9.9, +INFO[2021-09-16T08:56:33+08:00] Try to ping 8.8.8.8,1.1.1.1,9.9.9.9, +3 / 3 [-----------------------------------------------------------------------------------------] 100.00% 3 p/s 1.3s + +8.8.8.8 time: 55.4175ms +9.9.9.9 time: 71.2685ms +1.1.1.1 time: 196.496ms + +``` + +- Run the `ping` command with the parameter `-o P -d www.google.com` to get the parameters needed for the `download` command + +``` shell +./m3u8-downloader ping 8.8.8.8,1.1.1.1,9.9.9.9, -d www.google.com -o p +INFO[2021-09-16T08:57:21+08:00] Try to ping 8.8.8.8,1.1.1.1,9.9.9.9, +3 / 3 [-----------------------------------------------------------------------------------------] 100.00% 3 p/s 1.3s + +-C 'www.google.com:8.8.8.8' -C 'www.google.com:9.9.9.9' -C 'www.google.com:1.1.1.1' + +``` + +- Copy `-C 'www.google.com:8.8.8.8' -C 'www.google.com:9.9.9.9' -C 'www.google.com:1.1.1.1'` to the `download` command + +## Download command + +``` shell +./m3u8-downloader download -h + +All ts segments will be downloaded into a folder then be joined into a single TS file. + +Usage: + m3u8-downloader download [flags] + +Aliases: + download, d + +Flags: + -u, --baseUrl string base url for m3u8. + -C, --cdn stringArray CDN(s) for the download domain, eg. -C 'www.google.com:8.8.8.8' -C 'www.google.com:1.1.1.1' -C 'www.google.com:9.9.9.9' . + -d, --deleteSyncByte some TS files do not start with SyncByte 0x47, they can not be played after merging, need to remove the bytes before the SyncByte. + -D, --deleteTS delete all the downloaded TS file. (default true) + -f, --downloadDir string download directory, base on current folder. (default "./outputFolder") + -H, --header stringArray custom http header(s), eg. -H 'user-agent: Mozilla/5.0...' -H 'accept: */*' . + -h, --help help for download + --key string custom key to decrypt ts data. + --keyFormat string format of key, format can be those values: original, hex, base64. (default "original") + -l, --logLevel string logging level on a Logger,logging levels: Trace, Debug, Info, Warning, Error, Fatal and Panic. (default "Info") + -o, --output string file name for save. + -x, --proxy string use proxy. eg. http://127.0.0.1:8080 + -n, --threadNumber int the number of download thread. (default 10) + +Global Flags: + --config string config file (default is $HOME/.m3u8-downloader.yaml) +``` + +### Quickly copy download information from chrome + +- Open Chrome's developer tools, find the m3u8 request, right click `Copy`->`Copy as cURL`, as follows + +![9](https://user-images.githubusercontent.com/802316/133644904-aa049868-e1fc-40d6-b7b8-85f615f86c07.png) + +- Replace `curl` with `. /m3u8-downloader download`, and remove `--compressed` if it has it + +``` shell +./m3u8-downloader download 'https://abc.com/def.m3u8' \ + -H 'sec-ch-ua: "Microsoft Edge";v="93", " Not;A Brand";v="99", "Chromium";v="93"' \ + -H 'accept: text/html' \ + -H 'dnt: 1' \ + -H 'x-requested-with: XMLHttpRequest' \ + -H 'user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36 Edg/93.0.961.47' \ + -H 'accept-language: en,en-US;q=0.9' \ +``` + +## Thanks + +- [https://github.com/Greyh4t/m3u8-Downloader-Go](https://github.com/Greyh4t/m3u8-Downloader-Go) +- [https://github.com/llychao/m3u8-downloader](https://github.com/llychao/m3u8-downloader) diff --git a/README.zh-cn.md b/README.zh-cn.md new file mode 100644 index 0000000..45bde1f --- /dev/null +++ b/README.zh-cn.md @@ -0,0 +1,173 @@ +# m3u8-downloader + +[![构建状态](https://github.com/cxjava/m3u8-downloader/actions/workflows/build-and-test.yml/badge.svg)](https://github.com/cxjava/m3u8-downloader/actions/workflows/build-and-test.yml) +[![img](https://img.shields.io/github/license/cxjava/m3u8-downloader?label=%E8%AE%B8%E5%8F%AF%E8%AF%81)](https://github.com/cxjava/m3u8-downloader) +[![img](https://img.shields.io/github/release/cxjava/m3u8-downloader?label=%E6%9C%80%E6%96%B0%E7%89%88%E6%9C%AC)](https://github.com/cxjava/m3u8-downloader/releases) +![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/cxjava/m3u8-downloader) + +[README](README.md) | 简体中文 + +Golang版本的M3U8下载器 + +![m3u8-downloader-ScreenShot](https://user-images.githubusercontent.com/802316/133533481-483aa464-2fbe-4a25-9539-4a6345481dcd.png) + +## 功能特性 + +- 支持CDN下载,突破速度限制 +- 支持自定义的key,key的格式可以是hex或者base64,或者原始 +- 支持自定义的host前缀 +- 从文件读取m3u8或者网络地址读取m3u8 +- 支持代理 +- 支持自定义请求头 +- 是否删除下载的ts文件 +- 自定义线程数 +- 自定义下载文件名 +- 进度条 +- 支持在Apple M1,路由器上运行 + +## TODO + +- [ ] 支持边下边解密 +- [ ] ffmpeg 处理命令 +- [ ] 显示总文件大小 +- [ ] 删除出错CDN IP + +## Ping命令使用方法 + +``` shell +./m3u8-downloader p -h + +With this command, you can ping multiple IPs to detect IP response speeds. Easily get the fastest CDN IPs.IP is separated by commas, for example: ./m3u8-downloader p 8.8.8.8,1.1.1.1,9.9.9.9, The have two output format, default output is speed of each IP, for example: + +./m3u8-downloader p 8.8.8.8,1.1.1.1,9.9.9.9, + +9.9.9.9 time: 23.9635ms +8.8.8.8 time: 61.2325ms +1.1.1.1 time: 225.7115ms + +Other output is the parameter of download m3u8, for example: + +./m3u8-downloader p 8.8.8.8,1.1.1.1,9.9.9.9, -d www.google.com -o p + +-C 'www.google.com:8.8.8.8' -C 'www.google.com:1.1.1.1' -C 'www.google.com:9.9.9.9' + +Usage: + m3u8-downloader ping [flags] + +Aliases: + ping, p + +Flags: + -d, --domain string Only avaliable when output type is 'p', domain to use output as a parameter, such as: -C 'www.google.com:1.1.1.1' -C 'www.google.com:8.8.8.8' (default "www.google.com") + -h, --help help for ping + -l, --logLevel string logging level on a Logger,logging levels: Trace, Debug, Info, Warning, Error, Fatal and Panic. (default "Info") + -o, --outputType string output type, can be 't' or 'p', if the type is 'p', it will print "-C 'www.example.com:1.1.1.1' -C 'www.example.com:9.9.9.9'". If the type is 't', it will print 8.8.8.8 time: 62.17ms (default "t") + +Global Flags: + --config string config file (default is $HOME/.m3u8-downloader.yaml) +``` + +### 如何利用CDN进行下载 + +- 打开 [https://ping.chinaz.com/](https://ping.chinaz.com/) 输入需要查找IP的域名地址,Ping结束后,点击复制按钮 + +![1](https://user-images.githubusercontent.com/802316/133531905-ac398cc4-77da-44e3-a309-351feebd0628.png) + +- 运行 `ping` 命令查询每个IP的ping时间 + +``` shell +./m3u8-downloader p 8.8.8.8,1.1.1.1,9.9.9.9, +INFO[2021-09-16T08:56:33+08:00] Try to ping 8.8.8.8,1.1.1.1,9.9.9.9, +3 / 3 [-----------------------------------------------------------------------------------------] 100.00% 3 p/s 1.3s + +8.8.8.8 time: 55.4175ms +9.9.9.9 time: 71.2685ms +1.1.1.1 time: 196.496ms + +``` + +- 运行 `ping`命令带上参数`-o P -d www.google.com`得到`download`命令需要的参数 + +``` shell +./m3u8-downloader ping 8.8.8.8,1.1.1.1,9.9.9.9, -d www.google.com -o p +INFO[2021-09-16T08:57:21+08:00] Try to ping 8.8.8.8,1.1.1.1,9.9.9.9, +3 / 3 [-----------------------------------------------------------------------------------------] 100.00% 3 p/s 1.3s + +-C 'www.google.com:8.8.8.8' -C 'www.google.com:9.9.9.9' -C 'www.google.com:1.1.1.1' + +``` + +- 复制`-C 'www.google.com:8.8.8.8' -C 'www.google.com:9.9.9.9' -C 'www.google.com:1.1.1.1'`到`download`命令里面 + +## Download命令使用方法 + +``` shell +./m3u8-downloader download -h + +All ts segments will be downloaded into a folder then be joined into a single TS file. + +Usage: + m3u8-downloader download [flags] + +Aliases: + download, d + +Flags: + -u, --baseUrl string base url for m3u8. + -C, --cdn stringArray CDN(s) for the download domain, eg. -C 'www.google.com:8.8.8.8' -C 'www.google.com:1.1.1.1' -C 'www.google.com:9.9.9.9' . + -d, --deleteSyncByte some TS files do not start with SyncByte 0x47, they can not be played after merging, need to remove the bytes before the SyncByte. + -D, --deleteTS delete all the downloaded TS file. (default true) + -f, --downloadDir string download directory, base on current folder. (default "./outputFolder") + -H, --header stringArray custom http header(s), eg. -H 'user-agent: Mozilla/5.0...' -H 'accept: */*' . + -h, --help help for download + --key string custom key to decrypt ts data. + --keyFormat string format of key, format can be those values: original, hex, base64. (default "original") + -l, --logLevel string logging level on a Logger,logging levels: Trace, Debug, Info, Warning, Error, Fatal and Panic. (default "Info") + -o, --output string file name for save. + -x, --proxy string use proxy. eg. http://127.0.0.1:8080 + -n, --threadNumber int the number of download thread. (default 10) + +Global Flags: + --config string config file (default is $HOME/.m3u8-downloader.yaml) +``` + +### 参数说明 + +``` shell + -u, --baseUrl string 覆盖下载m3u8的host + -C, --cdn stringArray CDN参数,例子如: -C 'www.google.com:8.8.8.8' -C 'www.google.com:1.1.1.1' -C 'www.google.com:9.9.9.9' . + -d, --deleteSyncByte TS二包头,参见: https://en.wikipedia.org/wiki/MPEG_transport_stream . + -D, --deleteTS 删除下载的TS文件,默认是true + -f, --downloadDir string 下载的目录. (默认是 "./outputFolder") + -H, --header stringArray 自定义请求头,参数兼容curl命令的参数,可以用chrome开发者工具,复制为cUrl命令。例如: -H 'user-agent: Mozilla/5.0...' -H 'accept: */*' . + -h, --help 帮助命令 + --key string 自定义解密的key. + --keyFormat string 自定义秘钥的格式,有原始格式,hex格式,base64格式: original, hex, base64. (默认 "original") + -l, --logLevel string 日志级别: Trace, Debug, Info, Warning, Error, Fatal and Panic. (默认 "Info") + -o, --output string 下载之后的文件名称,建议设置为ts结尾. + -x, --proxy string 设置代理下载. 比如:http://127.0.0.1:8080 + -n, --threadNumber int 下载请求的线程数量. (默认 10) +``` + +### 快速从chrome中复制下载信息 + +- 打开chrome的开发者工具,找到m3u8那个请求,右键单击`复制`-->`复制为cURL`,如下图 + +![2](https://user-images.githubusercontent.com/802316/133640083-8a632552-0af5-464f-9720-e5e866f9fbcf.png) + +- 替换`curl` 为 `./m3u8-downloader download`, 如果有 `--compressed` 也去掉 + +``` shell +./m3u8-downloader download 'https://abc.com/def.m3u8' \ + -H 'sec-ch-ua: "Microsoft Edge";v="93", " Not;A Brand";v="99", "Chromium";v="93"' \ + -H 'accept: text/html' \ + -H 'dnt: 1' \ + -H 'x-requested-with: XMLHttpRequest' \ + -H 'user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36 Edg/93.0.961.47' \ + -H 'accept-language: en,en-US;q=0.9' \ +``` + +## 感谢以下项目 + +- [https://github.com/Greyh4t/m3u8-Downloader-Go](https://github.com/Greyh4t/m3u8-Downloader-Go) +- [https://github.com/llychao/m3u8-downloader](https://github.com/llychao/m3u8-downloader) diff --git a/cmd/download.go b/cmd/download.go new file mode 100644 index 0000000..db624e3 --- /dev/null +++ b/cmd/download.go @@ -0,0 +1,113 @@ +package cmd + +import ( + "fmt" + "strings" + + "github.com/cxjava/m3u8-downloader/downloader" + "github.com/golang-module/carbon" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +var ( + downloadDir string + downloadUrl string + baseUrl string + deleteSyncByte bool + deleteTS bool + proxy string + output string + threadNumber int + headers []string + cdns []string + logLevel string + key string + keyFormat string +) + +// downloadCmd represents the download command +var downloadCmd = &cobra.Command{ + Use: "download", + Aliases: []string{"d"}, + Short: "download the m3u8 and save it", + Long: `All ts segments will be downloaded into a folder then be joined into a single TS file.`, + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + initLog() + downloadUrl = args[0] + if len(output) == 0 { + output = fmt.Sprintf("%d.ts", carbon.Now().TimestampWithMillisecond()) + } + log.Info("output file name is :", output) + options := downloader.Options{ + DownloadUrl: downloadUrl, + Output: output, + DownloadDir: downloadDir, + BaseUrl: baseUrl, + Proxy: proxy, + DeleteSyncByte: deleteSyncByte, + DeleteTS: deleteTS, + ThreadNumber: threadNumber, + Headers: headers, + CDNs: cdns, + Key: key, + KeyFormat: keyFormat, + } + downloader.SetOptions(options) + downloader.Download() + }, +} + +func init() { + rootCmd.AddCommand(downloadCmd) + + downloadCmd.Flags().StringVarP(&output, "output", "o", "", "file name for save.") + downloadCmd.Flags().StringVarP(&downloadDir, "downloadDir", "f", "./outputFolder", "download directory, base on current folder.") + downloadCmd.Flags().StringVarP(&baseUrl, "baseUrl", "u", "", "base url for m3u8.") + downloadCmd.Flags().StringVarP(&proxy, "proxy", "x", "", "use proxy. eg. http://127.0.0.1:8080") + downloadCmd.Flags().BoolVarP(&deleteSyncByte, "deleteSyncByte", "d", false, "some TS files do not start with SyncByte 0x47, they can not be played after merging, need to remove the bytes before the SyncByte.") + downloadCmd.Flags().BoolVarP(&deleteTS, "deleteTS", "D", true, "delete all the downloaded TS file.") + downloadCmd.Flags().IntVarP(&threadNumber, "threadNumber", "n", 10, "the number of download thread.") + downloadCmd.Flags().StringArrayVarP(&headers, "header", "H", nil, "custom http header(s), eg. -H 'user-agent: Mozilla/5.0...' -H 'accept: */*' .") + downloadCmd.Flags().StringArrayVarP(&cdns, "cdn", "C", nil, "CDN(s) for the download domain, eg. -C 'www.google.com:8.8.8.8' -C 'www.google.com:1.1.1.1' -C 'www.google.com:9.9.9.9' .") + downloadCmd.Flags().StringVarP(&logLevel, "logLevel", "l", "Info", "logging level on a Logger,logging levels: Trace, Debug, Info, Warning, Error, Fatal and Panic.") + downloadCmd.Flags().StringVarP(&key, "key", "", "", "custom key to decrypt ts data.") + downloadCmd.Flags().StringVarP(&keyFormat, "keyFormat", "", "original", "format of key, format can be those values: original, hex, base64.") + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // downloadCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // downloadCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} + +func initLog() { + log.SetFormatter(&log.TextFormatter{ + DisableColors: false, + FullTimestamp: true, + }) + // log.SetFormatter(&log.JSONFormatter{}) + switch strings.ToLower(logLevel) { + case "trace": + log.SetLevel(log.TraceLevel) + case "debug": + log.SetLevel(log.DebugLevel) + case "info": + log.SetLevel(log.InfoLevel) + case "warning": + log.SetLevel(log.WarnLevel) + case "error": + log.SetLevel(log.ErrorLevel) + case "fatal": + log.SetLevel(log.FatalLevel) + case "panic": + log.SetLevel(log.PanicLevel) + default: + log.SetLevel(log.InfoLevel) + } +} diff --git a/cmd/ping.go b/cmd/ping.go new file mode 100644 index 0000000..fce3d38 --- /dev/null +++ b/cmd/ping.go @@ -0,0 +1,71 @@ +package cmd + +import ( + "strings" + "time" + + "github.com/cxjava/m3u8-downloader/ping" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +var ( + outputType string + host string + ip string +) + +// pingCmd represents the ping command +var pingCmd = &cobra.Command{ + Use: "ping", + Aliases: []string{"p"}, + Short: "With this command, you can ping multiple IPs to detect IP response speeds. Easily get the fastest CDN IPs.", + Long: `With this command, you can ping multiple IPs to detect IP response speeds. Easily get the fastest CDN IPs. +IP is separated by commas, for example: ./m3u8-downloader p 8.8.8.8,1.1.1.1,9.9.9.9, +The have two output format, default output is speed of each IP, for example: + +./m3u8-downloader p 8.8.8.8,1.1.1.1,9.9.9.9, + +9.9.9.9 time: 23.9635ms +8.8.8.8 time: 61.2325ms +1.1.1.1 time: 225.7115ms + +Other output is the parameter of download m3u8, for example: + +./m3u8-downloader p 8.8.8.8,1.1.1.1,9.9.9.9, -d www.google.com -o p + +-C 'www.google.com:8.8.8.8' -C 'www.google.com:1.1.1.1' -C 'www.google.com:9.9.9.9' + +`, + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + initLog() + ip = args[0] + log.Info("Try to ping " + ip) + ipList := strings.Split(ip, ",") + pingRecords := []ping.PingRecord{} + for _, ip := range ipList { + if len(strings.TrimSpace(ip)) > 0 { + pingRecords = append(pingRecords, ping.PingRecord{ + IPAddress: strings.TrimSpace(ip), + PingRTT: 5 * time.Second, + }) + } + } + option := ping.Options{ + PingRecords: pingRecords, + Host: host, + OutPutType: outputType, + } + ping.SetOptions(option) + ping.Ping() + }, +} + +func init() { + rootCmd.AddCommand(pingCmd) + + pingCmd.Flags().StringVarP(&outputType, "outputType", "o", "t", `output type, can be 't' or 'p', if the type is 'p', it will print "-C 'www.example.com:1.1.1.1' -C 'www.example.com:9.9.9.9'". If the type is 't', it will print 8.8.8.8 time: 62.17ms `) + pingCmd.Flags().StringVarP(&host, "domain", "d", "www.google.com", "Only avaliable when output type is 'p', domain to use output as a parameter, such as: -C 'www.google.com:1.1.1.1' -C 'www.google.com:8.8.8.8' ") + pingCmd.Flags().StringVarP(&logLevel, "logLevel", "l", "Info", "logging level on a Logger,logging levels: Trace, Debug, Info, Warning, Error, Fatal and Panic.") +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..5f921dd --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,78 @@ +package cmd + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + + "github.com/spf13/viper" +) + +var cfgFile string + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Use: "m3u8-downloader", + Short: "m3u8 downloader by Golang", + Long: `A tool to help you download a m3u8 file and save it as a single TS file +Features: + +* ping IPS +* support multiple thread download +* support CDN +* support proxy +* support custom header +* progress bar +* custom download file name +* decrypt hls encoded segments +* auto retry download +* ...`, + // Uncomment the following line if your bare application + // has an action associated with it: + // Run: func(cmd *cobra.Command, args []string) { }, +} + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + cobra.CheckErr(rootCmd.Execute()) +} + +func init() { + cobra.OnInitialize(initConfig) + + // Here you will define your flags and configuration settings. + // Cobra supports persistent flags, which, if defined here, + // will be global for your application. + + rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.m3u8-downloader.yaml)") + + // Cobra also supports local flags, which will only run + // when this action is called directly. + rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} + +// initConfig reads in config file and ENV variables if set. +func initConfig() { + if cfgFile != "" { + // Use config file from the flag. + viper.SetConfigFile(cfgFile) + } else { + // Find home directory. + home, err := os.UserHomeDir() + cobra.CheckErr(err) + + // Search config in home directory with name ".m3u8-downloader" (without extension). + viper.AddConfigPath(home) + viper.SetConfigType("yaml") + viper.SetConfigName(".m3u8-downloader") + } + + viper.AutomaticEnv() // read in environment variables that match + + // If a config file is found, read it in. + if err := viper.ReadInConfig(); err == nil { + fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed()) + } +} diff --git a/cmd/version.go b/cmd/version.go new file mode 100644 index 0000000..3645cb1 --- /dev/null +++ b/cmd/version.go @@ -0,0 +1,36 @@ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +var ( + Version = "dev" + Commit = "none" + RepoUrl = "unknown" + BuildDate = "unknown" + BuiltBy = "unknown" + BuiltWithGoVersion = "unknown" +) + +// versionCmd represents the version command +var versionCmd = &cobra.Command{ + Use: "version", + Aliases: []string{"v"}, + Short: "Show the version.", + Long: `Display the version.`, + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("Version: \t" + Version) + fmt.Println("Repo URL: \t" + RepoUrl) + fmt.Println("Commit Info: \t" + Commit) + fmt.Println("Build Time: \t" + BuildDate) + fmt.Println("Build By: \t" + BuiltBy) + fmt.Println("Go Version: \t" + BuiltWithGoVersion) + }, +} + +func init() { + rootCmd.AddCommand(versionCmd) +} diff --git a/decrypter/decrypt.go b/decrypter/decrypt.go new file mode 100644 index 0000000..1968ed5 --- /dev/null +++ b/decrypter/decrypt.go @@ -0,0 +1,24 @@ +package decrypter + +import ( + "crypto/aes" + "crypto/cipher" +) + +func Decrypt(data, key, iv []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + cbc := cipher.NewCBCDecrypter(block, iv) + cbc.CryptBlocks(data, data) + + return PKCS7UnPadding(data), nil +} + +func PKCS7UnPadding(origData []byte) []byte { + length := len(origData) + unpadding := int(origData[length-1]) + return origData[:(length - unpadding)] +} diff --git a/downloader/cdn.go b/downloader/cdn.go new file mode 100644 index 0000000..585bd1e --- /dev/null +++ b/downloader/cdn.go @@ -0,0 +1,59 @@ +package downloader + +import ( + "strings" + + log "github.com/sirupsen/logrus" +) + +var cdnChan = make(map[string]chan string, 2) + +type CDNS struct { + Domain string + IPs []string +} + +func initCDN(cdns []string) { + cdnList := parseCDN(cdns) + cdnChan = addCDN(cdnList) +} + +func parseCDN(cdns []string) map[string]CDNS { + log.Trace("Parse CDN") + fastestCDN := make(map[string]CDNS, 2) + + for _, v := range cdns { + dp := strings.Split(v, ":") + domain := dp[0] + ip := dp[1] + if cdns, ok := fastestCDN[domain]; ok { + log.Trace("Exist CDNS for domain: " + domain + ", IP: " + ip) + cdns.IPs = append(cdns.IPs, ip) + fastestCDN[domain] = cdns + } else { + log.Trace("Create CDNS for domain: " + domain + ", IP: " + ip) + fastestCDN[domain] = CDNS{ + Domain: domain, + IPs: []string{ip}, + } + } + } + return fastestCDN +} + +func addCDN(cmap map[string]CDNS) map[string]chan string { + log.Trace("Add CDN") + cdnMap := make(map[string]chan string, len(cmap)) + for _, cdn := range cmap { + ipChan := make(chan string, 10) + go func(ipChan chan string, ips []string) { + for { + for _, v := range ips { + ipChan <- v + } + } + }(ipChan, cdn.IPs) + cdnMap[cdn.Domain] = ipChan + } + return cdnMap +} diff --git a/downloader/cdn_test.go b/downloader/cdn_test.go new file mode 100644 index 0000000..839105c --- /dev/null +++ b/downloader/cdn_test.go @@ -0,0 +1,76 @@ +package downloader + +import ( + "fmt" + "testing" +) + +func Test_parseCDN(t *testing.T) { + type args struct { + cdns []string + } + tests := []struct { + name string + args args + want map[string]CDNS + }{ + { + name: "get the original URI", + args: args{ + cdns: []string{"www.google1.com:8.8.8.8", "www.google2.com:1.1.1.1", "www.google1.com:9.9.9.9", "www.google2.com:7.9.9.9", "www.google4.com:7.7.9.9"}, + }, + want: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := parseCDN(tt.args.cdns) + if len(got) != 3 { + t.Errorf("parseCDN() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_addCDN(t *testing.T) { + type args struct { + cdnmap map[string]CDNS + } + tests := []struct { + name string + args args + want map[string]chan string + }{ + { + name: "get the cdn", + args: args{ + cdnmap: map[string]CDNS{ + "1": { + Domain: "www.google.com", + IPs: []string{"1.1.1.1", "2.2.2.2", "5.5.5.5", "6.6.6.6"}, + }, + "3": { + Domain: "www.google2.com", + IPs: []string{"3.3.3.3", "4.4.4.4"}, + }, + "5": { + Domain: "www.baidu.com", + IPs: []string{"bbb", "ddd", "eeee", "ffff"}, + }, + }, + }, + want: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := addCDN(tt.args.cdnmap) + for i := 0; i < 10; i++ { + if v, ok := got["www.baidu.com"]; ok { + fmt.Println(<-v) + } + } + t.Logf("Done!") + }) + } +} diff --git a/downloader/httpclient.go b/downloader/httpclient.go new file mode 100644 index 0000000..eb09aad --- /dev/null +++ b/downloader/httpclient.go @@ -0,0 +1,95 @@ +package downloader + +import ( + "context" + "crypto/tls" + "fmt" + "net" + "net/http" + "runtime" + "strings" + "time" + + "github.com/go-resty/resty/v2" + log "github.com/sirupsen/logrus" +) + +var ( + client = resty.New() + defaultHeaders = map[string]string{ + "dnt": "1", + "Accept": "*/*", + "Accept-Charset": "UTF-8,*;q=0.5", + "Accept-Encoding": "gzip,deflate,sdch", + "Accept-Language": "en-US,en;q=0.8", + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36 Edg/91.0.864.54", + } +) + +func download(url string) ([]byte, error) { + log.Trace("Start download " + url) + resp, err := client.R().Get(url) + log.Trace("Finish download " + url) + log.Tracef("Error :%+v", err) + log.Trace("Status :" + resp.Status()) + return resp.Body(), err +} + +func initHttpClient(proxy string, headers []string) { + log.Trace("Init http client") + dialer := &net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + } + transport := &http.Transport{ + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + // addr is "google.com:443" + domainAndPort := strings.Split(addr, ":") + domain := domainAndPort[0] + if ipch, ok := cdnChan[domain]; ok { + ip := <-ipch + port := domainAndPort[1] + addr = fmt.Sprintf("%s:%s", ip, port) + log.Debug("Use cdn address :", addr) + } + return dialer.DialContext(ctx, network, addr) + }, + Proxy: http.ProxyFromEnvironment, + ForceAttemptHTTP2: true, + DisableKeepAlives: true, // for CDN + MaxIdleConns: 100, + IdleConnTimeout: 5 * time.Second, + TLSHandshakeTimeout: 5 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + MaxIdleConnsPerHost: runtime.GOMAXPROCS(0) + 1, + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + client.SetTransport(transport) + + client.SetTimeout(5 * time.Second) + + if len(proxy) > 0 { + log.Info("Use proxy : ", proxy) + client.SetProxy(proxy) + } + + if len(headers) > 0 { + log.Info("Set customization header") + for _, header := range headers { + h := strings.SplitN(header, ":", 2) + log.Debug("customization header =>" + h[0] + ":" + h[1]) + client.SetHeader(h[0], strings.TrimSpace(h[1])) + } + } else { + log.Debug("Use default header") + log.Tracef("Use default header :%+v", defaultHeaders) + client.SetHeaders(defaultHeaders) + } + client. + SetRetryCount(5). + SetRetryWaitTime(5 * time.Second). + SetRetryMaxWaitTime(20 * time.Second) + + client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(10)) + +} diff --git a/downloader/m3u8.go b/downloader/m3u8.go new file mode 100644 index 0000000..4e4951a --- /dev/null +++ b/downloader/m3u8.go @@ -0,0 +1,347 @@ +package downloader + +import ( + "bytes" + "encoding/base64" + "encoding/hex" + "errors" + "fmt" + "io/ioutil" + "net/url" + "os" + "path/filepath" + "runtime" + "strings" + "sync" + + "github.com/cheggaaa/pb/v3" + "github.com/cxjava/m3u8-downloader/decrypter" + "github.com/cxjava/m3u8-downloader/utils" + "github.com/grafov/m3u8" + log "github.com/sirupsen/logrus" +) + +var ( + tmpl = `{{ "Downloading:" | rndcolor }} {{string . "prefix" | rndcolor }}{{counters . | rndcolor }} {{bar . | rndcolor }} {{percent . | rndcolor }} {{speed . | rndcolor }} {{rtime . "ETA %s"| rndcolor }}{{string . "suffix"| rndcolor }}` + syncByte = uint8(71) //0x47 + outputPath string +) + +var ( + downloadUrl string + output string + downloadDir string + proxy string + deleteSyncByte bool + deleteTS bool + threadNumber int + headers []string + cdns []string + baseUrl string + keyStr string + keyFormat string +) + +// Options defines common m3u8 options. +type Options struct { + DownloadUrl string + Output string + DownloadDir string + Proxy string + DeleteSyncByte bool + DeleteTS bool + ThreadNumber int + Headers []string + CDNs []string + BaseUrl string + Key string + KeyFormat string +} + +// SetOptions sets the common request option. +func SetOptions(opt Options) { + downloadUrl = opt.DownloadUrl + output = opt.Output + downloadDir = opt.DownloadDir + proxy = opt.Proxy + deleteSyncByte = opt.DeleteSyncByte + deleteTS = opt.DeleteTS + threadNumber = opt.ThreadNumber + headers = opt.Headers + cdns = opt.CDNs + baseUrl = opt.BaseUrl + keyStr = opt.Key + keyFormat = opt.KeyFormat +} + +func Download() { + initCDN(cdns) + initHttpClient(proxy, headers) + checkOutputFolder() + var data []byte + var err error + if strings.HasPrefix(downloadUrl, "http") { + data, err = download(downloadUrl) + if err != nil { + log.Error("Download m3u8 failed:" + downloadUrl + ",Error:" + err.Error()) + return + } + } else { + // read from file + data, err = ioutil.ReadFile(downloadUrl) + if err != nil { + log.Error("Read m3u8 file failed:" + downloadUrl + ",Error:" + err.Error()) + return + } + if len(baseUrl) == 0 { + log.Warn("make sure ts file have full path in the m3u8 file") + } + } + + mpl, err := parseM3u8(data, downloadUrl) + if err != nil { + log.Error("Parse m3u8 file failed:" + err.Error()) + return + } else { + log.Info("Parse m3u8 file successfully") + } + + downloadM3u8(mpl) + mergeFile() + renameFile() +} + +func renameFile() { + path1 := filepath.Join(outputPath, "/", "merged.tmp") + path2 := filepath.Join(downloadDir, "/", output) + err := os.Rename(path1, path2) + if err != nil { + log.Println("[error] Rename failed: " + err.Error()) + } + if deleteTS { + os.RemoveAll(outputPath) + } +} + +func mergeFile() { + switch runtime.GOOS { + case "windows": + utils.WinMergeFile(outputPath, deleteTS) + default: + utils.UnixMergeFile(outputPath, deleteTS) + } +} + +func checkOutputFolder() { + log.Trace("Check output folder") + pwd, _ := os.Getwd() + downloadDir, _ = filepath.Abs(filepath.Join(pwd, "/", downloadDir)) + log.Trace("Download dir is : " + downloadDir) + outputPath, _ = filepath.Abs(filepath.Join(downloadDir, "/", output+"_downloading")) + log.Trace("Output path is : " + outputPath) + utils.MkAllDir(outputPath) +} + +func parseM3u8(data []byte, downloadUrl string) (*m3u8.MediaPlaylist, error) { + log.Debug("Parse m3u8") + playlist, listType, err := m3u8.Decode(*bytes.NewBuffer(data), false) + if err != nil { + log.Error("Decode m3u8 failed: " + err.Error()) + return nil, err + } + + if listType == m3u8.MEDIA { + var baseHost *url.URL + if len(baseUrl) > 0 { + baseHost, err = url.Parse(baseUrl) + if err != nil { + log.Error("url.Parse(" + baseUrl + ") failed: " + err.Error()) + return nil, errors.New("parse base url failed: " + err.Error()) + } + } else if len(downloadUrl) > 0 { + baseHost, err = url.Parse(downloadUrl) + if err != nil { + log.Error("url.Parse(" + downloadUrl + ") failed: " + err.Error()) + return nil, errors.New("parse m3u8 url failed: " + err.Error()) + } + } + log.Trace("Base host is " + baseHost.String()) + + mpl := playlist.(*m3u8.MediaPlaylist) + + if mpl.Key != nil && mpl.Key.URI != "" { + uri, err := formatURI(baseHost, mpl.Key.URI) + if err != nil { + log.Error("formatURI(" + mpl.Key.URI + ") failed: " + err.Error()) + return nil, err + } + log.Trace("MPL key URI is " + uri) + mpl.Key.URI = uri + } + + total := int(mpl.Count()) + for i := 0; i < total; i++ { + segment := mpl.Segments[i] + + uri, err := formatURI(baseHost, segment.URI) + if err != nil { + log.Error("formatURI(" + segment.URI + ") failed: " + err.Error()) + return nil, err + } + log.Trace("Segment URI is " + uri) + segment.URI = uri + + if segment.Key != nil && segment.Key.URI != "" { + uri, err := formatURI(baseHost, segment.Key.URI) + if err != nil { + log.Error("formatURI(" + segment.Key.URI + ") failed: " + err.Error()) + return nil, err + } + log.Trace("Segment key URI is " + uri) + segment.Key.URI = uri + } + + mpl.Segments[i] = segment + } + return mpl, nil + } + + return nil, errors.New("unsupport m3u8 type") +} + +func downloadM3u8(mpl *m3u8.MediaPlaylist) { + + var wg sync.WaitGroup + threadLimiter := make(chan struct{}, threadNumber) + + var total = int(mpl.Count()) + bar := pb.ProgressBarTemplate(tmpl).Start64(int64(total)) + + for i := 0; i < total; i++ { + wg.Add(1) + threadLimiter <- struct{}{} + go func(index int, segment *m3u8.MediaSegment, globalKey *m3u8.Key) { + defer func() { + bar.Increment() + wg.Done() + <-threadLimiter + log.Trace("args ...interface{}") + }() + curr_path := fmt.Sprintf("%s/%05d.ts", outputPath, index) + if utils.IsExist(curr_path) { + log.Warn("File: " + curr_path + " already exist") + return + } + var keyURL, ivStr string + if segment.Key != nil && segment.Key.URI != "" { + keyURL = segment.Key.URI + ivStr = segment.Key.IV + } else if globalKey != nil && globalKey.URI != "" { + keyURL = globalKey.URI + ivStr = globalKey.IV + } + log.Trace("keyURL is " + keyURL + ", ivStr is " + ivStr) + + data, err := download(segment.URI) + if err != nil { + log.Error("Download : " + segment.URI + " failed: " + err.Error()) + } + + var originalData []byte + + if len(keyStr) > 0 { + log.Info("Try to decrypt data by custom key " + keyStr) + var key, iv []byte + if ivStr != "" { + iv, err = hex.DecodeString(strings.TrimPrefix(ivStr, "0x")) + if err != nil { + log.Error("Decode iv failed:" + err.Error()) + } + } else { + iv = []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, byte(index)} + } + switch strings.ToLower(keyFormat) { + case "original": + key = []byte(keyStr) + case "hex": + key = utils.HexDecode(keyStr) + case "base64": + var err error + key, err = base64.StdEncoding.DecodeString(keyStr) + if err != nil { + log.Errorf("base64 Decode %s Failed: %s.", keyStr, err.Error()) + } + default: + key = []byte(keyStr) + } + + originalData, err = decrypter.Decrypt(data, key, iv) + if err != nil { + log.Errorf("Decrypt failed by own key %s : %s", keyStr, err.Error()) + } + } else if keyURL == "" { + originalData = data + } else { + log.Info("Try to decrypt data") + var key, iv []byte + key, err = download(keyURL) + if err != nil { + log.Error("Download : " + keyURL + " failed: " + err.Error()) + } + + if ivStr != "" { + iv, err = hex.DecodeString(strings.TrimPrefix(ivStr, "0x")) + if err != nil { + log.Error("Decode iv failed:" + err.Error()) + } + } else { + iv = []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, byte(index)} + } + originalData, err = decrypter.Decrypt(data, key, iv) + if err != nil { + log.Error("Decrypt failed:" + err.Error()) + } + } + + if deleteSyncByte { + log.Info("Delete sync byte.") + // https://en.wikipedia.org/wiki/MPEG_transport_stream + // Some TS files do not start with SyncByte 0x47, they can not be played after merging, + // Need to remove the bytes before the SyncByte 0x47(71). + dataLength := len(originalData) + for j := 0; j < dataLength; j++ { + if originalData[j] == syncByte { + log.Warn("Find sync byte, and delete it.") + originalData = originalData[j:] + break + } + } + } + + err = ioutil.WriteFile(curr_path, originalData, 0666) + if err != nil { + log.Error("WriteFile failed:" + err.Error()) + } + log.Trace("Save file '" + curr_path + "' successfully!") + }(i, mpl.Segments[i], mpl.Key) + } + wg.Wait() + bar.Finish() +} + +func formatURI(base *url.URL, u string) (string, error) { + if strings.HasPrefix(u, "http") { + return u, nil + } + + if base == nil { + return "", errors.New("you must set m3u8 url for file to download") + } + + obj, err := base.Parse(u) + if err != nil { + return "", err + } + + return obj.String(), nil +} diff --git a/downloader/m3u8_test.go b/downloader/m3u8_test.go new file mode 100644 index 0000000..6623f06 --- /dev/null +++ b/downloader/m3u8_test.go @@ -0,0 +1,64 @@ +package downloader + +import ( + "net/url" + "testing" +) + +func Test_formatURI(t *testing.T) { + type args struct { + base *url.URL + u string + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + name: "get the format URI", + args: args{ + base: parseUrl("https://res001.geekbang.org/media/audio/51/02/51"), + u: "/124.ts", + }, + want: "https://res001.geekbang.org/124.ts", + wantErr: false, + }, + { + name: "get the original URI", + args: args{ + base: parseUrl("https://res001.geekbang.org/media/audio/51/02/51"), + u: "https://www.baidu.com/124.ts", + }, + want: "https://www.baidu.com/124.ts", + wantErr: false, + }, + { + name: "throw error when base is nil", + args: args{ + base: nil, + u: "124.ts", + }, + want: "", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := formatURI(tt.args.base, tt.args.u) + if (err != nil) != tt.wantErr { + t.Errorf("formatURI() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("formatURI() = %v, want %v", got, tt.want) + } + }) + } +} + +func parseUrl(u string) *url.URL { + ur, _ := url.Parse(u) + return ur +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..feb42b8 --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +module github.com/cxjava/m3u8-downloader + +go 1.17 + +require ( + github.com/cheggaaa/pb/v3 v3.0.8 + github.com/go-ping/ping v0.0.0-20210911151512-381826476871 + github.com/go-resty/resty/v2 v2.6.0 + github.com/golang-module/carbon v1.5.4 + github.com/grafov/m3u8 v0.11.1 + github.com/sirupsen/logrus v1.8.1 + github.com/spf13/cobra v1.2.1 + github.com/spf13/viper v1.8.1 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..6c7e38d --- /dev/null +++ b/go.sum @@ -0,0 +1,663 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/VividCortex/ewma v1.1.1 h1:MnEK4VOv6n0RSY4vtRe3h11qjxL3+t0B8yOL8iMXdcM= +github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cheggaaa/pb/v3 v3.0.8 h1:bC8oemdChbke2FHIIGy9mn4DPJ2caZYQnfbRqwmdCoA= +github.com/cheggaaa/pb/v3 v3.0.8/go.mod h1:UICbiLec/XO6Hw6k+BHEtHeQFzzBH4i2/qk/ow1EJTA= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg= +github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-ping/ping v0.0.0-20210911151512-381826476871 h1:wtjTfjwAR/BYYMJ+QOLI/3J/qGEI0fgrkZvgsEWK2/Q= +github.com/go-ping/ping v0.0.0-20210911151512-381826476871/go.mod h1:xIFjORFzTxqIV/tDVGO4eDy/bLuSyawEeojSm3GfRGk= +github.com/go-resty/resty/v2 v2.6.0 h1:joIR5PNLM2EFqqESUjCMGXrWmXNHEU9CEiK813oKYS4= +github.com/go-resty/resty/v2 v2.6.0/go.mod h1:PwvJS6hvaPkjtjNg9ph+VrSD92bi5Zq73w/BIH7cC3Q= +github.com/gobuffalo/envy v1.7.0 h1:GlXgaiBkmrYMHco6t4j7SacKO4XUjvh5pwXh0f4uxXU= +github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= +github.com/gobuffalo/logger v1.0.0/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs= +github.com/gobuffalo/packd v0.3.0 h1:eMwymTkA1uXsqxS0Tpoop3Lc0u3kTfiMBE6nKtQU4g4= +github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q= +github.com/gobuffalo/packr v1.30.1 h1:hu1fuVR3fXEZR7rXNW3h8rqSML8EVAf6KNm0NKO/wKg= +github.com/gobuffalo/packr v1.30.1/go.mod h1:ljMyFO2EcrnzsHsN99cvbq055Y9OhRrIaviy289eRuk= +github.com/gobuffalo/packr/v2 v2.5.1/go.mod h1:8f9c96ITobJlPzI44jj+4tHnEKNt0xXWSVlXRN9X1Iw= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-module/carbon v1.5.4 h1:VR9VwOxl7JM3UrcU2KMjiiC6Rp1KoGy6pHh+onS5twA= +github.com/golang-module/carbon v1.5.4/go.mod h1:M/TDTYPp3qWtW68u49dLDJOyGmls6L6BXdo/pyvkMaU= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= +github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/grafov/m3u8 v0.11.1 h1:igZ7EBIB2IAsPPazKwRKdbhxcoBKO3lO1UY57PZDeNA= +github.com/grafov/m3u8 v0.11.1/go.mod h1:nqzOkfBiZJENr52zTVd/Dcl03yzphIMbJqkXGu+u080= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/karrick/godirwalk v1.10.12/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls= +github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-runewidth v0.0.12 h1:Y41i/hVW3Pgwr8gV+J23B9YEY0zxjptBuCWEaxmAOow= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ= +github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.3.0 h1:RR9dF3JtopPvtkroDZuVD7qquD0bnHlKSqaQhgwt8yk= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY= +github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/cobra v1.2.1 h1:+KmjbUw1hriSNMF55oPrkZcb27aECyrj8V2ytv7kWDw= +github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/spf13/viper v1.8.1 h1:Kq1fyeebqsBfbjZj4EL7gj2IO0mMaiyjYUWcUsl2O44= +github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= +go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190624180213-70d37148ca0c/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU= +gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/main.go b/main.go new file mode 100644 index 0000000..aa4a2ec --- /dev/null +++ b/main.go @@ -0,0 +1,7 @@ +package main + +import "github.com/cxjava/m3u8-downloader/cmd" + +func main() { + cmd.Execute() +} diff --git a/ping/ping.go b/ping/ping.go new file mode 100644 index 0000000..96d7c55 --- /dev/null +++ b/ping/ping.go @@ -0,0 +1,118 @@ +package ping + +import ( + "fmt" + "sort" + "sync" + "time" + + "github.com/cheggaaa/pb/v3" + "github.com/go-ping/ping" + log "github.com/sirupsen/logrus" +) + +var ( + pingRecords = []PingRecord{} + outputType = "p" + host = "www.google.com" +) + +type PingRecord struct { + IPAddress string + PingRTT time.Duration +} + +type Options struct { + PingRecords []PingRecord + OutPutType string + Host string +} + +func SetOptions(opt Options) { + pingRecords = opt.PingRecords + if len(opt.OutPutType) > 0 { + outputType = opt.OutPutType + } + if len(opt.Host) > 0 { + host = opt.Host + } +} +func Ping() { + PingAndPrint(pingRecords, outputType) +} + +func PingAndPrint(pingRecords []PingRecord, outputType string) { + var wg sync.WaitGroup + threadLimiter := make(chan struct{}, 10) + + var total = int(len(pingRecords)) + bar := pb.Full.Start(total) + + pingedRecords := []PingRecord{} + for _, pingRecord := range pingRecords { + wg.Add(1) + threadLimiter <- struct{}{} + go func(pingRecord PingRecord) { + defer func() { + bar.Increment() + wg.Done() + <-threadLimiter + log.Trace("Finished ping " + pingRecord.IPAddress) + }() + pingedRecords = append(pingedRecords, pingIP(pingRecord)) + }(pingRecord) + } + wg.Wait() + bar.Finish() + + sort.Slice(pingedRecords, func(i, j int) bool { + return pingedRecords[i].PingRTT < pingedRecords[j].PingRTT + }) + + if outputType == "t" { + outputTime(pingedRecords) + } else if outputType == "p" { + outputParameter(pingedRecords) + } +} + +func pingIP(pingRecord PingRecord) PingRecord { + pinger, err := ping.NewPinger(pingRecord.IPAddress) + if err != nil { + log.Error("Ping IP Error: " + err.Error()) + panic(err) + } + pinger.Count = 2 + pinger.Timeout = 2 * time.Second + err = pinger.Run() // blocks until finished + if err != nil { + log.Error("Ping Run Error: " + err.Error()) + panic(err) + } + stats := pinger.Statistics() // get send/receive/rtt stats + pingRecord.PingRTT = stats.AvgRtt + return pingRecord +} + +func outputTime(pingRecords []PingRecord) { + log.Trace("Print all ping time") + fmt.Println("") + for _, pingRecord := range pingRecords { + if pingRecord.PingRTT > 0 { + fmt.Printf("%s time: %s\n", pingRecord.IPAddress, pingRecord.PingRTT) + } + } + fmt.Println("") +} + +func outputParameter(pingRecords []PingRecord) { + log.Trace("Print as parameter for downloading") + fmt.Println("") + for _, pingRecord := range pingRecords { + if pingRecord.PingRTT > 0 { + fmt.Printf(`-C '%s:%s' `, host, pingRecord.IPAddress) + } + } + fmt.Println("") + fmt.Println("") +} diff --git a/ping/ping_test.go b/ping/ping_test.go new file mode 100644 index 0000000..85df403 --- /dev/null +++ b/ping/ping_test.go @@ -0,0 +1,104 @@ +package ping + +import ( + "testing" + "time" +) + +func Test_pingIP(t *testing.T) { + type args struct { + pingRecord PingRecord + } + tests := []struct { + name string + args args + }{ + { + name: "ping 1.1.1.1", + args: args{ + pingRecord: PingRecord{ + IPAddress: "1.1.1.1", + PingRTT: time.Duration(1), + }, + }, + }, + { + name: "ping 8.8.8.8", + args: args{ + pingRecord: PingRecord{ + IPAddress: "8.8.8.8", + PingRTT: time.Duration(1), + }, + }, + }, + { + name: "ping 114.114.114.114", + args: args{ + pingRecord: PingRecord{ + IPAddress: "114.114.114.114", + PingRTT: time.Duration(1), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pingIP(tt.args.pingRecord) + }) + } + +} + +func Test_outputParameter(t *testing.T) { + type args struct { + pingRecords []PingRecord + } + tests := []struct { + name string + args args + }{ + { + name: "output to parameters", + args: args{ + pingRecords: []PingRecord{ + {"1.1.1.1", 2 * time.Microsecond}, + {"8.8.8.8", 88 * time.Microsecond}, + {"8.8.4.4", 99 * time.Microsecond}, + {"114.114.114.114", 0 * time.Microsecond}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + outputParameter(tt.args.pingRecords) + }) + } +} + +func Test_outputTime(t *testing.T) { + type args struct { + pingRecords []PingRecord + } + tests := []struct { + name string + args args + }{ + { + name: "sort by time", + args: args{ + pingRecords: []PingRecord{ + {"1.1.1.1", 1 * time.Microsecond}, + {"8.8.8.8", 28 * time.Microsecond}, + {"8.8.4.4", 33 * time.Microsecond}, + {"114.114.114.114", 0 * time.Microsecond}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + outputTime(tt.args.pingRecords) + }) + } +} diff --git a/utils/file.go b/utils/file.go new file mode 100644 index 0000000..d145f69 --- /dev/null +++ b/utils/file.go @@ -0,0 +1,58 @@ +package utils + +import ( + "io/ioutil" + "os" + "path" + "path/filepath" + "strings" + + log "github.com/sirupsen/logrus" +) + +func MkAllDir(dirs string) { + if !IsExist(dirs) { + err := os.MkdirAll(dirs, os.ModePerm) + check(err) + } +} + +func FilenameWithoutExtension(fn string) string { + return strings.TrimSuffix(fn, path.Ext(fn)) +} + +func RemoveAllDir(dirs string) { + os.RemoveAll(strings.Split(dirs, "/")[0]) +} + +func IsExist(filePath string) bool { + _, err := os.Stat(filePath) + if err != nil { + if os.IsExist(err) { + return true + } + log.Trace("Warn: " + filePath + " is not exist, error: " + err.Error()) + return false + } + return true +} + +func WriteFile(fileName string, data []byte) { + err := ioutil.WriteFile(fileName, data, os.ModePerm) + check(err) +} + +func check(err error) { + if err != nil { + log.Error("error :" + err.Error()) + panic(err) + } +} + +func FileAbs(path string) string { + pwd, _ := os.Getwd() + if absPath, err := filepath.Abs(filepath.Join(pwd, path)); err == nil { + return absPath + } + return "" +} diff --git a/utils/file_test.go b/utils/file_test.go new file mode 100644 index 0000000..f8a89b6 --- /dev/null +++ b/utils/file_test.go @@ -0,0 +1,177 @@ +package utils + +import ( + "fmt" + "log" + "os" + "path/filepath" + "strings" + "testing" +) + +func TestMkAllDir(t *testing.T) { + type args struct { + dirs string + } + tests := []struct { + name string + args args + }{ + { + name: "create test folder successfully", + args: args{ + dirs: "test/test", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + MkAllDir(tt.args.dirs) + RemoveAllDir(tt.args.dirs) + }) + } +} + +func TestRemoveAllDir(t *testing.T) { + type args struct { + dirs string + } + tests := []struct { + name string + args args + }{ + { + name: "remove folder successfully", + args: args{ + dirs: "test/test/testaa", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + MkAllDir(tt.args.dirs) + RemoveAllDir(tt.args.dirs) + if IsExist(tt.args.dirs) { + log.Fatalln("remove failed") + } + }) + } + pwd, _ := os.Getwd() + fmt.Println(filepath.Join(pwd + "/./abc")) + fmt.Println(filepath.Join(pwd + "/../../def")) + fmt.Println(filepath.Join(pwd + "/abc")) + fmt.Println(filepath.Join(pwd + "abcd")) +} + +func TestIsExist(t *testing.T) { + type args struct { + filePath string + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "Not exist such path", + args: args{ + filePath: "test/test", + }, + want: false, + }, + { + name: "exist such file", + args: args{ + filePath: "file.go", + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := IsExist(tt.args.filePath); got != tt.want { + t.Errorf("IsExist() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestFileAbs(t *testing.T) { + type args struct { + path string + } + tests := []struct { + name string + args args + want string + }{ + { + name: "exist such file.go", + args: args{ + path: "../file.go", + }, + want: "m3u8-downloader/file.go", + }, + { + name: "exist such file abc", + args: args{ + path: "./../abc", + }, + want: "m3u8-downloader/abc", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := FileAbs(tt.args.path); !strings.HasSuffix(got, tt.want) { + t.Errorf("FileAbs() = %v, want end with %v", got, tt.want) + } + }) + } +} + +func TestFilenameWithoutExtension(t *testing.T) { + type args struct { + fn string + } + tests := []struct { + name string + args args + want string + }{ + { + name: "get same file name", + args: args{ + fn: "../.././../abc", + }, + want: "../.././../abc", + }, + { + name: "get the same file name", + args: args{ + fn: "afasdf-adf_adfas", + }, + want: "afasdf-adf_adfas", + }, + { + name: "get def.exe", + args: args{ + fn: "def.exe.def", + }, + want: "def.exe", + }, + { + name: "get abc", + args: args{ + fn: "abc.txt", + }, + want: "abc", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := FilenameWithoutExtension(tt.args.fn); got != tt.want { + t.Errorf("FilenameWithoutExtension() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/utils/hex.go b/utils/hex.go new file mode 100644 index 0000000..6c1b164 --- /dev/null +++ b/utils/hex.go @@ -0,0 +1,21 @@ +package utils + +import ( + "encoding/hex" + + log "github.com/sirupsen/logrus" +) + +func HexDecode(s string) []byte { + decoded, err := hex.DecodeString(s) + if err != nil { + log.Error("HexDecode failed:" + err.Error()) + return nil + } + log.Tracef("HexDecode %s", decoded) + return decoded +} + +func HexEncode(s []byte) string { + return hex.EncodeToString(s) +} diff --git a/utils/hex_test.go b/utils/hex_test.go new file mode 100644 index 0000000..f2e243c --- /dev/null +++ b/utils/hex_test.go @@ -0,0 +1,73 @@ +package utils + +import ( + "reflect" + "testing" +) + +func TestHexDecode(t *testing.T) { + type args struct { + s string + } + tests := []struct { + name string + args args + want []byte + }{ + { + name: "decode hello", + args: args{ + s: "68656c6c6f", + }, + want: []byte{104, 101, 108, 108, 111}, + }, + + { + name: "decode m3u8Golang", + args: args{ + s: "6d337538476f6c616e67", + }, + want: []byte("m3u8Golang"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := HexDecode(tt.args.s); !reflect.DeepEqual(got, tt.want) { + t.Errorf("HexDecode() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestHexEncode(t *testing.T) { + type args struct { + s []byte + } + tests := []struct { + name string + args args + want string + }{ + { + name: "encode hello", + args: args{ + s: []byte("hello"), + }, + want: "68656c6c6f", + }, + { + name: "encode m3u8Golang", + args: args{ + s: []byte("m3u8Golang"), + }, + want: "6d337538476f6c616e67", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := HexEncode(tt.args.s); got != tt.want { + t.Errorf("HexEncode() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/utils/shell.go b/utils/shell.go new file mode 100644 index 0000000..cac847a --- /dev/null +++ b/utils/shell.go @@ -0,0 +1,70 @@ +package utils + +import ( + "bytes" + "os" + "os/exec" + + log "github.com/sirupsen/logrus" +) + +func ExecUnixShell(s string) error { + cmd := exec.Command("/bin/bash", "-c", s) + var out bytes.Buffer + cmd.Stdout = &out + err := cmd.Run() + if err != nil { + log.Error("ExecUnixShell Error: " + err.Error()) + return err + } + outStr := out.String() + if len(outStr) > 0 { + log.Info(outStr) + } + return nil +} + +func ExecWinShell(s string) error { + cmd := exec.Command("cmd", "/C", s) + var out bytes.Buffer + cmd.Stdout = &out + err := cmd.Run() + if err != nil { + log.Error("ExecWinShell Error: " + err.Error()) + return err + } + outStr := out.String() + if len(outStr) > 0 { + log.Info(outStr) + } + return nil +} + +//windows合并文件 +func WinMergeFile(path string, del bool) { + err := os.Chdir(path) + check(err) + log.Info("Copy all ts files to merged.tmp") + err = ExecWinShell("copy /b *.ts merged.tmp") + check(err) + if del { + log.Warn("Delete all ts files") + err = ExecWinShell("del /Q *.ts") + check(err) + } +} + +//unix合并文件 +func UnixMergeFile(path string, del bool) { + err := os.Chdir(path) + check(err) + log.Info("Copy all ts files to merged.tmp") + cmd := `cat *.ts >> merged.tmp` + err = ExecUnixShell(cmd) + check(err) + if del { + log.Warn("Delete all ts files") + err = ExecUnixShell("rm -rf *.ts") + check(err) + } +} diff --git a/utils/shell_test.go b/utils/shell_test.go new file mode 100644 index 0000000..94d72f1 --- /dev/null +++ b/utils/shell_test.go @@ -0,0 +1,38 @@ +package utils + +import ( + "testing" +) + +func TestExecUnixShell(t *testing.T) { + type args struct { + s string + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "echo hello successfully", + args: args{ + s: "echo hello", + }, + wantErr: false, + }, + { + name: "exec failed", + args: args{ + s: "xxx bbb", + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := ExecUnixShell(tt.args.s); (err != nil) != tt.wantErr { + t.Errorf("ExecUnixShell() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +}