diff --git a/.github/workflows/go.yaml b/.github/workflows/go.yaml index 7deb8194..06f593ad 100644 --- a/.github/workflows/go.yaml +++ b/.github/workflows/go.yaml @@ -8,8 +8,8 @@ jobs: lint: strategy: matrix: - platform: [ubuntu-22.04] - go-version: [1.21.x, 1.x] + platform: [ubuntu-24.04] + go-version: [1.26.x, 1.x] runs-on: ${{ matrix.platform }} name: Linters (Static Analysis) for Go steps: @@ -26,8 +26,8 @@ jobs: test: strategy: matrix: - platform: [ubuntu-22.04] - go-version: [1.21.x, 1.x] + platform: [ubuntu-24.04] + go-version: [1.26.x, 1.x] runs-on: ${{ matrix.platform }} name: integration tests env: @@ -50,7 +50,7 @@ jobs: run: make test-integration - name: Archive artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: output path: output diff --git a/.github/workflows/schedule.yaml b/.github/workflows/schedule.yaml index d3752da1..1e141673 100644 --- a/.github/workflows/schedule.yaml +++ b/.github/workflows/schedule.yaml @@ -8,8 +8,8 @@ jobs: test: strategy: matrix: - platform: [ubuntu-22.04] - go-version: [1.21.x] + platform: [ubuntu-24.04] + go-version: [1.26.x] runs-on: ${{ matrix.platform }} name: integration tests env: @@ -32,7 +32,7 @@ jobs: run: make test-integration - name: Archive artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: output path: output diff --git a/.golangci.yml b/.golangci.yml index c0319a2f..0a5109d3 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,8 +1,25 @@ -issues: - exclude: - - Subprocess launched with function call - # Excluding configuration per-path, per-linter, per-text and per-source - exclude-rules: - # Exclude some linters from running on tests files. - - path: cmd/ - text: InsecureSkipVerify +version: "2" +linters: + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + rules: + - path: cmd/ + text: InsecureSkipVerify + - path: (.+)\.go$ + text: Subprocess launched with function call + paths: + - third_party$ + - builtin$ + - examples$ +formatters: + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/.goreleaser.yml b/.goreleaser.yml index f7d7c44c..e39e38a1 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,3 +1,4 @@ +version: 2 project_name: youtubedr env: @@ -18,3 +19,6 @@ builds: - arm64 flags: - -trimpath + ignore: + - goos: windows + goarch: arm diff --git a/Makefile b/Makefile index 02b6aedf..220bc2df 100644 --- a/Makefile +++ b/Makefile @@ -27,7 +27,7 @@ deps: lint: command -v golangci-lint || curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $$(go env GOPATH)/bin $(GOLANGCI_LINT_VERSION) echo "golangci-lint checking..." - golangci-lint run --deadline=30m --enable=misspell --enable=gosec --enable=gofmt --enable=goimports --enable=revive ./cmd/... ./... + golangci-lint run --timeout=30m ./cmd/... ./... go vet ./... ## format: Formats Go code diff --git a/README.md b/README.md index 74de98b0..26eb7acf 100644 --- a/README.md +++ b/README.md @@ -19,20 +19,19 @@ This tool is meant to be used to download CC0 licenced content, we do not suppor ## Installation -### Install via go get - -Please ensure you have installed Go 1.21 or later. +### Run Manually ```shell -go get github.com/kkdai/youtube/v2 +git clone https://github.com/kkdai/youtube.git && cd youtube +go run ./cmd/youtubedr ``` -### From source code +### Install via Go + +Please ensure you have installed Go 1.26 or later. ```shell -git clone https://github.com/kkdai/youtube.git -cd youtube -go run ./cmd/youtubedr +go install github.com/kkdai/youtube/v2/cmd/youtubedr@latest ``` ### Mac @@ -56,8 +55,8 @@ pkg install youtubedr It's really simple to use, just get the video id from youtube url - ex: `https://www.youtube.com/watch?v=rFejpH_tAHM`, the video id is `rFejpH_tAHM` ```shell -$ youtubedr download QAGDGja7kbs -$ youtubedr download https://www.youtube.com/watch?v=rFejpH_tAHM +youtubedr download rFejpH_tAHM +youtubedr download https://www.youtube.com/watch?v=rFejpH_tAHM ``` @@ -69,8 +68,6 @@ Please check out the [example_test.go](example_test.go) for example code. ## Example: * ### Get information of dotGo-2015-rob-pike video for downloading - `go get github.com/kkdai/youtube/v2/youtubedr` - Download video from [dotGo 2015 - Rob Pike - Simplicity is Complicated](https://www.youtube.com/watch?v=rFejpH_tAHM) ``` @@ -87,8 +84,6 @@ Please check out the [example_test.go](example_test.go) for example code. ``` * ### Download dotGo-2015-rob-pike-video - `go get github.com/kkdai/youtube/v2/youtubedr` - Download video from [dotGo 2015 - Rob Pike - Simplicity is Complicated](https://www.youtube.com/watch?v=rFejpH_tAHM) ``` @@ -97,8 +92,6 @@ Please check out the [example_test.go](example_test.go) for example code. * ### Download video to specific folder and name - `go get github.com/kkdai/youtube/v2/youtubedr` - Download video from [dotGo 2015 - Rob Pike - Simplicity is Complicated](https://www.youtube.com/watch?v=rFejpH_tAHM) to current directory and name the file to simplicity-is-complicated.mp4 ``` @@ -107,8 +100,6 @@ Please check out the [example_test.go](example_test.go) for example code. * ### Download video with specific quality - `go get github.com/kkdai/youtube/v2/youtubedr` - Download video from [dotGo 2015 - Rob Pike - Simplicity is Complicated](https://www.youtube.com/watch?v=rFejpH_tAHM) with specific quality ``` @@ -125,8 +116,6 @@ Please check out the [example_test.go](example_test.go) for example code. * ### Download video with specific itag - `go get github.com/kkdai/youtube/v2/youtubedr` - Download video from [dotGo 2015 - Rob Pike - Simplicity is Complicated](https://www.youtube.com/watch?v=rFejpH_tAHM) ``` diff --git a/client.go b/client.go index 428724a9..509085af 100644 --- a/client.go +++ b/client.go @@ -7,13 +7,14 @@ import ( "errors" "fmt" "io" + "log/slog" "math/rand" "net/http" "net/url" "strconv" + "strings" "sync/atomic" - - "log/slog" + "time" ) const ( @@ -24,12 +25,12 @@ const ( playerParams = "CgIQBg==" ) -var ( - ErrNoFormat = errors.New("no video format provided") -) +const ContentPlaybackNonceAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" + +var ErrNoFormat = errors.New("no video format provided") // DefaultClient type to use. No reason to change but you could if you wanted to. -var DefaultClient = AndroidClient +var DefaultClient = AndroidVRClient // Client offers methods to download video metadata and video streams. type Client struct { @@ -46,9 +47,14 @@ type Client struct { // playerCache caches the JavaScript code of a player response playerCache playerCache - client *clientInfo + client *ClientInfo consentID string + + visitorId struct { + value string + updated time.Time + } } func (c *Client) assureClient() { @@ -86,6 +92,19 @@ func (c *Client) videoFromID(ctx context.Context, id string) (*Video, error) { // return early if all good if err = v.parseVideoInfo(body); err == nil { + //We still need to get additional informations about the video from the HTML page + videoFromHtml := Video{ID: id} + html, err := c.httpGetBodyBytes(ctx, "https://www.youtube.com/watch?v="+id+"&bpctr=9999999999&has_verified=1") + if err != nil { + return nil, err + } + + err = videoFromHtml.parseVideoPage(html) + if err != nil { + return nil, err + } + v.PublishDate = videoFromHtml.PublishDate + return &v, nil } @@ -159,41 +178,60 @@ type innertubeClient struct { UserAgent string `json:"userAgent,omitempty"` TimeZone string `json:"timeZone"` UTCOffset int `json:"utcOffsetMinutes"` + DeviceModel string `json:"deviceModel,omitempty"` + VisitorData string `json:"visitorData,omitempty"` } // client info for the innertube API -type clientInfo struct { - name string - key string - version string - userAgent string - androidVersion int +type ClientInfo struct { + Name string + Key string + Version string + UserAgent string + AndroidVersion int + DeviceModel string } var ( // WebClient, better to use Android client but go ahead. - WebClient = clientInfo{ - name: "WEB", - version: "2.20210617.01.00", - key: "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8", - userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", + WebClient = ClientInfo{ + Name: "WEB", + Version: "2.20220801.00.00", + Key: "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8", + UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", } // AndroidClient, download go brrrrrr. - AndroidClient = clientInfo{ - name: "ANDROID", - version: "17.31.35", - key: "AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39w", - userAgent: "com.google.android.youtube/17.31.35 (Linux; U; Android 11) gzip", - androidVersion: 30, + AndroidClient = ClientInfo{ + Name: "ANDROID", + Version: "20.10.38", + Key: "AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39w", + UserAgent: "com.google.android.youtube/20.10.38 (Linux; U; Android 11) gzip", + // androidVersion removed to avoid PoToken requirement (android_sdkless variant) + } + + // IOSClient Client based brrrr. + IOSClient = ClientInfo{ + Name: "IOS", + Version: "19.45.4", + Key: "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8", + UserAgent: "com.google.ios.youtube/19.45.4 (iPhone16,2; U; CPU iOS 18_1_0 like Mac OS X;)", + DeviceModel: "iPhone16,2", } // EmbeddedClient, not really tested. - EmbeddedClient = clientInfo{ - name: "WEB_EMBEDDED_PLAYER", - version: "1.19700101", - key: "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8", // seems like same key works for both clients - userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", + EmbeddedClient = ClientInfo{ + Name: "WEB_EMBEDDED_PLAYER", + Version: "1.19700101", + Key: "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8", // seems like same key works for both clients + UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", + } + + AndroidVRClient = ClientInfo{ + Name: "ANDROID_VR", + Version: "1.65.10", //"7.20260114.12.00", + Key: "", + UserAgent: "com.google.android.apps.youtube.vr.oculus/1.65.10 (Linux; U; Android 12L; eureka-user Build/SQ3A.220605.009.A1) gzip", } ) @@ -203,7 +241,7 @@ func (c *Client) videoDataByInnertube(ctx context.Context, id string) ([]byte, e Context: prepareInnertubeContext(*c.client), ContentCheckOK: true, RacyCheckOk: true, - Params: playerParams, + // Params: playerParams, PlaybackContext: &playbackContext{ ContentPlaybackContext: contentPlaybackContext{ // SignatureTimestamp: sts, @@ -212,33 +250,62 @@ func (c *Client) videoDataByInnertube(ctx context.Context, id string) ([]byte, e }, } - return c.httpPostBodyBytes(ctx, "https://www.youtube.com/youtubei/v1/player?key="+c.client.key, data) + return c.httpPostBodyBytes(ctx, "https://www.youtube.com/youtubei/v1/player?key="+c.client.Key, data) } -func (c *Client) transcriptDataByInnertube(ctx context.Context, id string) ([]byte, error) { +func (c *Client) transcriptDataByInnertube(ctx context.Context, id string, lang string) ([]byte, error) { data := innertubeRequest{ Context: prepareInnertubeContext(*c.client), - Params: transcriptVideoID(id), + Params: transcriptVideoID(id, lang), } - return c.httpPostBodyBytes(ctx, "https://www.youtube.com/youtubei/v1/get_transcript?key="+c.client.key, data) + return c.httpPostBodyBytes(ctx, "https://www.youtube.com/youtubei/v1/get_transcript?key="+c.client.Key, data) +} + +func randString(alphabet string, sz int) string { + var buf strings.Builder + buf.Grow(sz) + for i := 0; i < sz; i++ { + buf.WriteByte(alphabet[rand.Intn(len(alphabet))]) + } + return buf.String() +} + +func randomVisitorData(countryCode string) string { + var pbE2 ProtoBuilder + + pbE2.String(2, "") + pbE2.Varint(4, int64(rand.Intn(255)+1)) + + var pbE ProtoBuilder + pbE.String(1, countryCode) + pbE.Bytes(2, pbE2.ToBytes()) + + var pb ProtoBuilder + pb.String(1, randString(ContentPlaybackNonceAlphabet, 11)) + pb.Varint(5, time.Now().Unix()-int64(rand.Intn(600000))) + pb.Bytes(6, pbE.ToBytes()) + + return pb.ToURLEncodedBase64() } -func prepareInnertubeContext(clientInfo clientInfo) inntertubeContext { +func prepareInnertubeContext(clientInfo ClientInfo) inntertubeContext { return inntertubeContext{ Client: innertubeClient{ HL: "en", GL: "US", TimeZone: "UTC", - ClientName: clientInfo.name, - ClientVersion: clientInfo.version, - AndroidSDKVersion: clientInfo.androidVersion, - UserAgent: clientInfo.userAgent, + DeviceModel: clientInfo.DeviceModel, + ClientName: clientInfo.Name, + ClientVersion: clientInfo.Version, + AndroidSDKVersion: clientInfo.AndroidVersion, + UserAgent: clientInfo.UserAgent, + VisitorData: randomVisitorData("US"), }, } } -func prepareInnertubePlaylistData(ID string, continuation bool, clientInfo clientInfo) innertubeRequest { +func prepareInnertubePlaylistData(ID string, continuation bool, clientInfo ClientInfo) innertubeRequest { context := prepareInnertubeContext(clientInfo) if continuation { @@ -261,8 +328,8 @@ func prepareInnertubePlaylistData(ID string, continuation bool, clientInfo clien } // transcriptVideoID encodes the video ID to the param used to fetch transcripts. -func transcriptVideoID(videoID string) string { - langCode := encTranscriptLang("en") +func transcriptVideoID(videoID string, lang string) string { + langCode := encTranscriptLang(lang) // This can be optionally appened to the Sprintf str, not sure what it means // *3engagement-panel-searchable-transcript-search-panel\x30\x00\x38\x01\x40\x01 @@ -293,7 +360,7 @@ func (c *Client) GetPlaylistContext(ctx context.Context, url string) (*Playlist, } data := prepareInnertubePlaylistData(id, false, *c.client) - body, err := c.httpPostBodyBytes(ctx, "https://www.youtube.com/youtubei/v1/browse?key="+c.client.key, data) + body, err := c.httpPostBodyBytes(ctx, "https://www.youtube.com/youtubei/v1/browse?key="+c.client.Key, data) if err != nil { return nil, err } @@ -427,7 +494,6 @@ func (c *Client) downloadChunked(ctx context.Context, req *http.Request, w *io.P return case data := <-chunks[i].data: _, err := io.Copy(w, bytes.NewBuffer(data)) - if err != nil { abort(err) } @@ -453,7 +519,7 @@ func (c *Client) GetStreamURLContext(ctx context.Context, video *Video, format * c.assureClient() if format.URL != "" { - if c.client.androidVersion > 0 { + if c.client.AndroidVersion > 0 { return format.URL, nil } @@ -482,7 +548,7 @@ func (c *Client) httpDo(req *http.Request) (*http.Response, error) { client = http.DefaultClient } - req.Header.Set("User-Agent", c.client.userAgent) + req.Header.Set("User-Agent", c.client.UserAgent) req.Header.Set("Origin", "https://youtube.com") req.Header.Set("Sec-Fetch-Mode", "navigate") @@ -501,6 +567,12 @@ func (c *Client) httpDo(req *http.Request) (*http.Response, error) { log := slog.With("method", req.Method, "url", req.URL) + if err == nil && res.StatusCode != http.StatusOK { + err = ErrUnexpectedStatusCode(res.StatusCode) + res.Body.Close() + res = nil + } + if err != nil { log.Debug("HTTP request failed", "error", err) } else { @@ -554,10 +626,16 @@ func (c *Client) httpPost(ctx context.Context, url string, body interface{}) (*h } req.Header.Set("X-Youtube-Client-Name", "3") - req.Header.Set("X-Youtube-Client-Version", c.client.version) + req.Header.Set("X-Youtube-Client-Version", c.client.Version) req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") + if xgoogvisitorid, err := c.getVisitorId(); err != nil { + return nil, err + } else { + req.Header.Set("x-goog-visitor-id", xgoogvisitorid) + } + resp, err := c.httpDo(req) if err != nil { return nil, err @@ -571,6 +649,57 @@ func (c *Client) httpPost(ctx context.Context, url string, body interface{}) (*h return resp, nil } +var VisitorIdMaxAge = 10 * time.Hour + +func (c *Client) getVisitorId() (string, error) { + var err error + if c.visitorId.value == "" || time.Since(c.visitorId.updated) > VisitorIdMaxAge { + err = c.refreshVisitorId() + } + + return c.visitorId.value, err +} + +func (c *Client) refreshVisitorId() error { + const sep = "\nytcfg.set(" + + req, err := http.NewRequest(http.MethodGet, "https://www.youtube.com", nil) + if err != nil { + return err + } + + resp, err := c.httpDo(req) + if err != nil { + return err + } + defer resp.Body.Close() + data, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + _, data1, found := strings.Cut(string(data), sep) + if !found { + return err + } + var value struct { + InnertubeContext struct { + Client struct { + VisitorData string + } + } `json:"INNERTUBE_CONTEXT"` + } + if err := json.NewDecoder(strings.NewReader(data1)).Decode(&value); err != nil { + return err + } + + if c.visitorId.value, err = url.PathUnescape(value.InnertubeContext.Client.VisitorData); err != nil { + return err + } + + c.visitorId.updated = time.Now() + return nil +} + // httpPostBodyBytes reads the whole HTTP body and returns it func (c *Client) httpPostBodyBytes(ctx context.Context, url string, body interface{}) ([]byte, error) { resp, err := c.httpPost(ctx, url, body) @@ -596,7 +725,7 @@ func (c *Client) downloadChunk(req *http.Request, chunk *chunk) error { } defer resp.Body.Close() - if resp.StatusCode < http.StatusOK && resp.StatusCode >= 300 { + if resp.StatusCode != http.StatusOK { return ErrUnexpectedStatusCode(resp.StatusCode) } diff --git a/client_test.go b/client_test.go index 391ccc5c..be71cf6c 100644 --- a/client_test.go +++ b/client_test.go @@ -131,7 +131,8 @@ func TestWebClientGetVideoWithoutManifestURL(t *testing.T) { assert.Contains(video.Description, "Go is often described as a simple language.") // Publishing date and channel handle are present in web client - assert.Equal("2015-12-02 00:00:00 +0000 UTC", video.PublishDate.String()) + //assert.Equal("2015-12-02 00:00:00 +0000 UTC", video.PublishDate.String()) + assert.Equal("@dotconferences", video.ChannelHandle) } @@ -155,10 +156,34 @@ func TestGetVideoWithManifestURL(t *testing.T) { assert.NotZero(size) } +func TestGetVideo_MultiLanguage(t *testing.T) { + assert, require := assert.New(t), require.New(t) + video, err := testClient.GetVideo("https://www.youtube.com/watch?v=pU9sHwNKc2c") + require.NoError(err) + require.NotNil(video) + + // collect languages + var languageNames, lanaguageIDs []string + for _, format := range video.Formats { + if format.AudioTrack != nil { + languageNames = append(languageNames, format.LanguageDisplayName()) + lanaguageIDs = append(lanaguageIDs, format.AudioTrack.ID) + } + } + + assert.Contains(languageNames, "English original") + assert.Contains(languageNames, "Portuguese (BR)") + assert.Contains(lanaguageIDs, "en.4") + assert.Contains(lanaguageIDs, "pt-BR.3") + + assert.Empty(video.Formats.Language("Does not exist")) + assert.NotEmpty(video.Formats.Language("English original")) +} + func TestGetStream(t *testing.T) { assert, require := assert.New(t), require.New(t) - expectedSize := 988479 + expectedSize := 1241837 // Create testclient to enforce re-using of routines testClient := Client{ @@ -171,7 +196,7 @@ func TestGetStream(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() - video, err := testClient.GetVideoContext(ctx, "https://www.youtube.com/watch?v=BaW_jenozKc") + video, err := testClient.GetVideoContext(ctx, "https://www.youtube.com/watch?v=ezfPHVOf-NE") require.NoError(err) require.NotNil(video) require.Greater(len(video.Formats), 0) @@ -199,7 +224,7 @@ func TestGetPlaylist(t *testing.T) { v := playlist.Videos[7] assert.Equal(v.ID, "dsUXAEzaC3Q") - assert.Equal(v.Title, "Michael Jackson - Bad (Shortened Version)") + assert.Equal(v.Title, "Michael Jackson - Bad") assert.Equal(v.Author, "Michael Jackson") assert.Equal(v.Duration, 4*time.Minute+20*time.Second) @@ -232,7 +257,7 @@ func TestClient_httpGetBodyBytes(t *testing.T) { {"unknown://", "unsupported protocol scheme"}, {"invalid\nurl", "invalid control character in URL"}, {"http://unknown-host/", "dial tcp"}, - {"http://example.com/does-not-exist", "unexpected status code: 404"}, + {"https://www.google.com/404", "unexpected status code: 404"}, {"http://example.com/", ""}, } for _, tt := range tests { diff --git a/cmd/youtubedr/download.go b/cmd/youtubedr/download.go index 93aeb49a..4425ee80 100644 --- a/cmd/youtubedr/download.go +++ b/cmd/youtubedr/download.go @@ -16,7 +16,7 @@ var downloadCmd = &cobra.Command{ Short: "Downloads a video from youtube", Example: `youtubedr -o "Campaign Diary".mp4 https://www.youtube.com/watch\?v\=XbNghLqsVwU`, Args: cobra.ExactArgs(1), - Run: func(cmd *cobra.Command, args []string) { + Run: func(_ *cobra.Command, args []string) { exitOnError(download(args[0])) }, } @@ -32,8 +32,7 @@ func init() { downloadCmd.Flags().StringVarP(&outputFile, "filename", "o", "", "The output file, the default is genated by the video title.") downloadCmd.Flags().StringVarP(&outputDir, "directory", "d", ".", "The output directory.") - addQualityFlag(downloadCmd.Flags()) - addMimeTypeFlag(downloadCmd.Flags()) + addVideoSelectionFlags(downloadCmd.Flags()) } func download(id string) error { @@ -48,7 +47,7 @@ func download(id string) error { if err := checkFFMPEG(); err != nil { return err } - return downloader.DownloadComposite(context.Background(), outputFile, video, outputQuality, mimetype) + return downloader.DownloadComposite(context.Background(), outputFile, video, outputQuality, mimetype, language) } return downloader.Download(context.Background(), video, format, outputFile) diff --git a/cmd/youtubedr/downloader.go b/cmd/youtubedr/downloader.go index 697f8280..fa4c37ff 100644 --- a/cmd/youtubedr/downloader.go +++ b/cmd/youtubedr/downloader.go @@ -2,7 +2,6 @@ package main import ( "crypto/tls" - "errors" "fmt" "net" "net/http" @@ -20,16 +19,15 @@ import ( var ( insecureSkipVerify bool // skip TLS server validation outputQuality string // itag number or quality string - mimetype string // mimetype + mimetype string + language string downloader *ytdl.Downloader ) -func addQualityFlag(flagSet *pflag.FlagSet) { +func addVideoSelectionFlags(flagSet *pflag.FlagSet) { flagSet.StringVarP(&outputQuality, "quality", "q", "medium", "The itag number or quality label (hd720, medium)") -} - -func addMimeTypeFlag(flagSet *pflag.FlagSet) { - flagSet.StringVarP(&mimetype, "mimetype", "m", "mp4", "Mime-Type to filter (mp4, webm, av01, avc1) - applicable if --quality used is quality label") + flagSet.StringVarP(&mimetype, "mimetype", "m", "", "Mime-Type to filter (mp4, webm, av01, avc1) - applicable if --quality used is quality label") + flagSet.StringVarP(&language, "language", "l", "", "Language to filter") } func getDownloader() *ytdl.Downloader { @@ -70,41 +68,34 @@ func getDownloader() *ytdl.Downloader { return downloader } -func getVideoWithFormat(id string) (*youtube.Video, *youtube.Format, error) { +func getVideoWithFormat(videoID string) (*youtube.Video, *youtube.Format, error) { dl := getDownloader() - video, err := dl.GetVideo(id) + video, err := dl.GetVideo(videoID) if err != nil { return nil, nil, err } + + itag, _ := strconv.Atoi(outputQuality) formats := video.Formats + + if language != "" { + formats = formats.Language(language) + } if mimetype != "" { formats = formats.Type(mimetype) } - if len(formats) == 0 { - return nil, nil, errors.New("no formats found") + if outputQuality != "" { + formats = formats.Quality(outputQuality) } - - var format *youtube.Format - itag, _ := strconv.Atoi(outputQuality) - switch { - case itag > 0: - // When an itag is specified, do not filter format with mime-type - format = video.Formats.FindByItag(itag) - if format == nil { - return nil, nil, fmt.Errorf("unable to find format with itag %d", itag) - } - - case outputQuality != "": - format = formats.FindByQuality(outputQuality) - if format == nil { - return nil, nil, fmt.Errorf("unable to find format with quality %s", outputQuality) - } - - default: - // select the first format - formats.Sort() - format = &formats[0] + if itag > 0 { + formats = formats.Itag(itag) + } + if formats == nil { + return nil, nil, fmt.Errorf("unable to find the specified format") } - return video, format, nil + formats.Sort() + + // select the first format + return video, &formats[0], nil } diff --git a/cmd/youtubedr/info.go b/cmd/youtubedr/info.go index 171fa0b7..d272b50b 100644 --- a/cmd/youtubedr/info.go +++ b/cmd/youtubedr/info.go @@ -18,6 +18,7 @@ type VideoFormat struct { VideoQuality string AudioQuality string AudioChannels int + Language string Size int64 Bitrate int MimeType string @@ -37,10 +38,10 @@ var infoCmd = &cobra.Command{ Use: "info", Short: "Print metadata of the desired video", Args: cobra.ExactArgs(1), - PreRunE: func(cmd *cobra.Command, args []string) error { + PreRunE: func(_ *cobra.Command, _ []string) error { return checkOutputFormat() }, - Run: func(cmd *cobra.Command, args []string) { + Run: func(_ *cobra.Command, args []string) { video, err := getDownloader().GetVideo(args[0]) exitOnError(err) @@ -73,6 +74,7 @@ var infoCmd = &cobra.Command{ Size: size, Bitrate: bitrate, MimeType: format.MimeType, + Language: format.LanguageDisplayName(), }) } @@ -102,6 +104,7 @@ func writeInfoOutput(w io.Writer, info *VideoInfo) { "size [MB]", "bitrate", "MimeType", + "language", }) for _, format := range info.Formats { @@ -114,6 +117,7 @@ func writeInfoOutput(w io.Writer, info *VideoInfo) { fmt.Sprintf("%0.1f", float64(format.Size)/1024/1024), strconv.Itoa(format.Bitrate), format.MimeType, + format.Language, }) } diff --git a/cmd/youtubedr/list.go b/cmd/youtubedr/list.go index ec7a3f9d..e7c58416 100644 --- a/cmd/youtubedr/list.go +++ b/cmd/youtubedr/list.go @@ -21,10 +21,10 @@ var ( Use: "list", Short: "Print metadata of the desired playlist", Args: cobra.ExactArgs(1), - PreRunE: func(cmd *cobra.Command, args []string) error { + PreRunE: func(_ *cobra.Command, _ []string) error { return checkOutputFormat() }, - Run: func(cmd *cobra.Command, args []string) { + Run: func(_ *cobra.Command, args []string) { playlist, err := getDownloader().GetPlaylist(args[0]) exitOnError(err) diff --git a/cmd/youtubedr/root.go b/cmd/youtubedr/root.go index 521d1d1f..891cc95a 100644 --- a/cmd/youtubedr/root.go +++ b/cmd/youtubedr/root.go @@ -20,7 +20,8 @@ var rootCmd = &cobra.Command{ Short: "Youtube downloader written in Golang", Long: `This tool is meant to be used to download CC0 licenced content, we do not support nor recommend using it for illegal activities. -Use the HTTP_PROXY environment variable to set a HTTP or SOCSK5 proxy. The proxy type is determined by the URL scheme. +Use the HTTPS_PROXY environment variable to set a proxy for requests to YouTube, since YouTube URLs are HTTPS. +The proxy type is determined by the proxy URL scheme. "http", "https", and "socks5" are supported. If the scheme is empty, "http" is assumed.`, // Uncomment the following line if your bare application // has an action associated with it: diff --git a/cmd/youtubedr/url.go b/cmd/youtubedr/url.go index 7afb698c..cbf23601 100644 --- a/cmd/youtubedr/url.go +++ b/cmd/youtubedr/url.go @@ -11,7 +11,7 @@ var urlCmd = &cobra.Command{ Use: "url", Short: "Only output the stream-url to desired video", Args: cobra.ExactArgs(1), - Run: func(cmd *cobra.Command, args []string) { + Run: func(_ *cobra.Command, args []string) { video, format, err := getVideoWithFormat(args[0]) exitOnError(err) @@ -23,7 +23,6 @@ var urlCmd = &cobra.Command{ } func init() { - addQualityFlag(urlCmd.Flags()) - addMimeTypeFlag(urlCmd.Flags()) + addVideoSelectionFlags(urlCmd.Flags()) rootCmd.AddCommand(urlCmd) } diff --git a/cmd/youtubedr/version.go b/cmd/youtubedr/version.go index ba359063..8b7333a5 100644 --- a/cmd/youtubedr/version.go +++ b/cmd/youtubedr/version.go @@ -18,7 +18,7 @@ var ( var versionCmd = &cobra.Command{ Use: "version", Short: "Prints version information", - Run: func(cmd *cobra.Command, args []string) { + Run: func(_ *cobra.Command, _ []string) { fmt.Println("Version: ", version) fmt.Println("Commit: ", commit) fmt.Println("Date: ", date) diff --git a/downloader/downloader.go b/downloader/downloader.go index 59491600..a766a972 100644 --- a/downloader/downloader.go +++ b/downloader/downloader.go @@ -2,8 +2,9 @@ package downloader import ( "context" - "fmt" + "errors" "io" + "log/slog" "os" "os/exec" "path/filepath" @@ -59,8 +60,8 @@ func (dl *Downloader) Download(ctx context.Context, v *youtube.Video, format *yo } // DownloadComposite : Downloads audio and video streams separately and merges them via ffmpeg. -func (dl *Downloader) DownloadComposite(ctx context.Context, outputFile string, v *youtube.Video, quality string, mimetype string) error { - videoFormat, audioFormat, err1 := getVideoAudioFormats(v, quality, mimetype) +func (dl *Downloader) DownloadComposite(ctx context.Context, outputFile string, v *youtube.Video, quality string, mimetype, language string) error { + videoFormat, audioFormat, err1 := getVideoAudioFormats(v, quality, mimetype, language) if err1 != nil { return err1 } @@ -85,14 +86,14 @@ func (dl *Downloader) DownloadComposite(ctx context.Context, outputFile string, if err != nil { return err } - defer os.Remove(videoFile.Name()) + defer closeAndRemoveFile(videoFile, log) // Create temporary audio file audioFile, err := os.CreateTemp(outputDir, "youtube_*.m4a") if err != nil { return err } - defer os.Remove(audioFile.Name()) + defer closeAndRemoveFile(audioFile, log) log.Debug("Downloading video file...") err = dl.videoDLWorker(ctx, videoFile, v, videoFormat) @@ -122,8 +123,18 @@ func (dl *Downloader) DownloadComposite(ctx context.Context, outputFile string, return ffmpegVersionCmd.Run() } -func getVideoAudioFormats(v *youtube.Video, quality string, mimetype string) (*youtube.Format, *youtube.Format, error) { - var videoFormat, audioFormat *youtube.Format +func closeAndRemoveFile(file *os.File, log *slog.Logger) { + if err := file.Close(); err != nil { + log.Error("Failed to close file", "error", err) + return + } + err := os.Remove(file.Name()) + if err != nil { + log.Error("Failed to delete file", "error", err) + } +} + +func getVideoAudioFormats(v *youtube.Video, quality string, mimetype, language string) (*youtube.Format, *youtube.Format, error) { var videoFormats, audioFormats youtube.FormatList formats := v.Formats @@ -138,25 +149,22 @@ func getVideoAudioFormats(v *youtube.Video, quality string, mimetype string) (*y videoFormats = videoFormats.Quality(quality) } - if len(videoFormats) > 0 { - videoFormats.Sort() - videoFormat = &videoFormats[0] + if language != "" { + audioFormats = audioFormats.Language(language) } - if len(audioFormats) > 0 { - audioFormats.Sort() - audioFormat = &audioFormats[0] + if len(videoFormats) == 0 { + return nil, nil, errors.New("no video format found after filtering") } - if videoFormat == nil { - return nil, nil, fmt.Errorf("no video format found after filtering") + if len(audioFormats) == 0 { + return nil, nil, errors.New("no audio format found after filtering") } - if audioFormat == nil { - return nil, nil, fmt.Errorf("no audio format found after filtering") - } + videoFormats.Sort() + audioFormats.Sort() - return videoFormat, audioFormat, nil + return &videoFormats[0], &audioFormats[0], nil } func (dl *Downloader) videoDLWorker(ctx context.Context, out *os.File, video *youtube.Video, format *youtube.Format) error { diff --git a/downloader/downloader_hq_test.go b/downloader/downloader_hq_test.go index 8a598e81..6cc0dd63 100644 --- a/downloader/downloader_hq_test.go +++ b/downloader/downloader_hq_test.go @@ -16,6 +16,5 @@ func TestDownload_HighQuality(t *testing.T) { video, err := testDownloader.Client.GetVideoContext(ctx, "BaW_jenozKc") require.NoError(err) - - require.NoError(testDownloader.DownloadComposite(ctx, "", video, "hd1080", "mp4")) + require.NoError(testDownloader.DownloadComposite(ctx, "", video, "hd1080", "mp4", "")) } diff --git a/downloader/downloader_test.go b/downloader/downloader_test.go index 367e897f..ca1cdefc 100644 --- a/downloader/downloader_test.go +++ b/downloader/downloader_test.go @@ -32,14 +32,14 @@ func TestDownload_FirstStream(t *testing.T) { ctx := context.Background() // youtube-dl test video - video, err := testDownloader.Client.GetVideoContext(ctx, "BaW_jenozKc") + video, err := testDownloader.GetVideoContext(ctx, "ezfPHVOf-NE") require.NoError(err) require.NotNil(video) - assert.Equal(`youtube-dl test video "'/\ä↭𝕐`, video.Title) - assert.Equal(`Philipp Hagemeister`, video.Author) - assert.Equal(10*time.Second, video.Duration) - assert.GreaterOrEqual(len(video.Formats), 18) + assert.Equal(`How to: Enable captions in Google Meet`, video.Title) + assert.Equal(`Google Workspace`, video.Author) + assert.Equal(13*time.Second, video.Duration) + assert.GreaterOrEqual(len(video.Formats), 8) if assert.Greater(len(video.Formats), 0) { assert.NoError(testDownloader.Download(ctx, video, &video.Formats[0], "")) @@ -69,7 +69,7 @@ func TestYoutube_DownloadWithHighQualityFails(t *testing.T) { Formats: tt.formats, } - err := testDownloader.DownloadComposite(context.Background(), "", video, "hd1080", "") + err := testDownloader.DownloadComposite(context.Background(), "", video, "hd1080", "", "") assert.EqualError(t, err, tt.message) }) } @@ -101,7 +101,7 @@ func Test_getVideoAudioFormats(t *testing.T) { {ItagNo: 249, MimeType: "audio/webm; codecs=\"opus\"", Quality: "tiny", Bitrate: 72862, FPS: 0, Width: 0, Height: 0, LastModified: "1540474783513282", ContentLength: 24839529, QualityLabel: "", ProjectionType: "RECTANGULAR", AverageBitrate: 55914, AudioQuality: "AUDIO_QUALITY_LOW", ApproxDurationMs: "3553941", AudioSampleRate: "48000", AudioChannels: 2}, }} { - videoFormat, audioFormat, err := getVideoAudioFormats(v, "hd720", "mp4") + videoFormat, audioFormat, err := getVideoAudioFormats(v, "hd720", "mp4", "") require.NoError(err) require.NotNil(videoFormat) require.Equal(398, videoFormat.ItagNo) @@ -110,7 +110,7 @@ func Test_getVideoAudioFormats(t *testing.T) { } { - videoFormat, audioFormat, err := getVideoAudioFormats(v, "large", "webm") + videoFormat, audioFormat, err := getVideoAudioFormats(v, "large", "webm", "") require.NoError(err) require.NotNil(videoFormat) require.Equal(244, videoFormat.ItagNo) diff --git a/errors_test.go b/errors_test.go index b3b68c34..f1d8beb9 100644 --- a/errors_test.go +++ b/errors_test.go @@ -8,6 +8,8 @@ import ( ) func TestErrors(t *testing.T) { + t.Parallel() + tests := []struct { err error expected string diff --git a/format_list.go b/format_list.go index 827d2f0d..57c98dc0 100644 --- a/format_list.go +++ b/format_list.go @@ -8,68 +8,59 @@ import ( type FormatList []Format -// FindByQuality returns the first format matching Quality or QualityLabel -// -// Examples: tiny, small, medium, large, 720p, hd720, hd1080 -func (list FormatList) FindByQuality(quality string) *Format { +// Type returns a new FormatList filtered by itag +func (list FormatList) Select(f func(Format) bool) (result FormatList) { for i := range list { - if list[i].Quality == quality || list[i].QualityLabel == quality { - return &list[i] + if f(list[i]) { + result = append(result, list[i]) } } - return nil + return result } -// FindByItag returns the first format matching the itag number -func (list FormatList) FindByItag(itagNo int) *Format { - for i := range list { - if list[i].ItagNo == itagNo { - return &list[i] - } - } - return nil +// Type returns a new FormatList filtered by itag +func (list FormatList) Itag(itagNo int) FormatList { + return list.Select(func(f Format) bool { + return f.ItagNo == itagNo + }) } -// Type returns a new FormatList filtered by mime type of video -func (list FormatList) Type(t string) (result FormatList) { - for i := range list { - if strings.Contains(list[i].MimeType, t) { - result = append(result, list[i]) - } - } - return result +// Type returns a new FormatList filtered by mime type +func (list FormatList) Type(value string) FormatList { + return list.Select(func(f Format) bool { + return strings.Contains(f.MimeType, value) + }) +} + +// Type returns a new FormatList filtered by display name +func (list FormatList) Language(displayName string) FormatList { + return list.Select(func(f Format) bool { + return f.LanguageDisplayName() == displayName + }) } // Quality returns a new FormatList filtered by quality, quality label or itag, // but not audio quality -func (list FormatList) Quality(quality string) (result FormatList) { - for _, f := range list { - itag, _ := strconv.Atoi(quality) - if itag == f.ItagNo || strings.Contains(f.Quality, quality) || strings.Contains(f.QualityLabel, quality) { - result = append(result, f) - } - } - return result +func (list FormatList) Quality(quality string) FormatList { + itag, _ := strconv.Atoi(quality) + + return list.Select(func(f Format) bool { + return itag == f.ItagNo || strings.Contains(f.Quality, quality) || strings.Contains(f.QualityLabel, quality) + }) } // AudioChannels returns a new FormatList filtered by the matching AudioChannels -func (list FormatList) AudioChannels(n int) (result FormatList) { - for _, f := range list { - if f.AudioChannels == n { - result = append(result, f) - } - } - return result +func (list FormatList) AudioChannels(n int) FormatList { + return list.Select(func(f Format) bool { + return f.AudioChannels == n + }) } // AudioChannels returns a new FormatList filtered by the matching AudioChannels -func (list FormatList) WithAudioChannels() (result FormatList) { - for _, f := range list { - if f.AudioChannels > 0 { - result = append(result, f) - } - } - return result +func (list FormatList) WithAudioChannels() FormatList { + return list.Select(func(f Format) bool { + return f.AudioChannels > 0 + }) } // FilterQuality reduces the format list to formats matching the quality @@ -86,7 +77,7 @@ func (list FormatList) Sort() { } // sortFormat sorts video by resolution, FPS, codec (av01, vp9, avc1), bitrate -// sorts audio by codec (mp4, opus), channels, bitrate, sample rate +// sorts audio by default, codec (mp4, opus), channels, bitrate, sample rate func sortFormat(i int, j int, formats FormatList) bool { // Sort by Width @@ -104,28 +95,34 @@ func sortFormat(i int, j int, formats FormatList) bool { if formats[i].FPS == formats[j].FPS { if formats[i].FPS == 0 && formats[i].AudioChannels > 0 && formats[j].AudioChannels > 0 { // Audio - // Sort by codec - codec := map[int]int{} - for _, index := range []int{i, j} { - if strings.Contains(formats[index].MimeType, "mp4") { - codec[index] = 1 - } else if strings.Contains(formats[index].MimeType, "opus") { - codec[index] = 2 + // Sort by default + if (formats[i].AudioTrack == nil && formats[j].AudioTrack == nil) || (formats[i].AudioTrack != nil && formats[j].AudioTrack != nil && formats[i].AudioTrack.AudioIsDefault == formats[j].AudioTrack.AudioIsDefault) { + // Sort by codec + codec := map[int]int{} + for _, index := range []int{i, j} { + if strings.Contains(formats[index].MimeType, "mp4") { + codec[index] = 1 + } else if strings.Contains(formats[index].MimeType, "opus") { + codec[index] = 2 + } } - } - if codec[i] == codec[j] { - // Sort by Audio Channel - if formats[i].AudioChannels == formats[j].AudioChannels { - // Sort by Audio Bitrate - if formats[i].Bitrate == formats[j].Bitrate { - // Sort by Audio Sample Rate - return formats[i].AudioSampleRate > formats[j].AudioSampleRate + if codec[i] == codec[j] { + // Sort by Audio Channel + if formats[i].AudioChannels == formats[j].AudioChannels { + // Sort by Audio Bitrate + if formats[i].Bitrate == formats[j].Bitrate { + // Sort by Audio Sample Rate + return formats[i].AudioSampleRate > formats[j].AudioSampleRate + } + return formats[i].Bitrate > formats[j].Bitrate } - return formats[i].Bitrate > formats[j].Bitrate + return formats[i].AudioChannels > formats[j].AudioChannels } - return formats[i].AudioChannels > formats[j].AudioChannels + return codec[i] < codec[j] + } else if formats[i].AudioTrack != nil && formats[i].AudioTrack.AudioIsDefault { + return true } - return codec[i] < codec[j] + return false } // Video // Sort by codec diff --git a/format_list_test.go b/format_list_test.go index b134ac89..2a5e0b0a 100644 --- a/format_list_test.go +++ b/format_list_test.go @@ -6,156 +6,130 @@ import ( "github.com/stretchr/testify/assert" ) -func TestFormatList_FindByQuality(t *testing.T) { - list := []Format{{ - ItagNo: 0, +type filter struct { + Quality string + ItagNo int + MimeType string + Language string + AudioChannels int +} + +func (list FormatList) Filter(filter filter) FormatList { + if filter.ItagNo > 0 { + list = list.Itag(filter.ItagNo) + } + if filter.AudioChannels > 0 { + list = list.AudioChannels(filter.AudioChannels) + } + if filter.Quality != "" { + list = list.Quality(filter.Quality) + } + if filter.MimeType != "" { + list = list.Type(filter.MimeType) + } + if filter.Language != "" { + list = list.Language(filter.Language) + } + return list +} + +func TestFormatList_Filter(t *testing.T) { + t.Parallel() + + format1 := Format{ + ItagNo: 1, Quality: "medium", QualityLabel: "360p", - }, - { - ItagNo: 1, - Quality: "large", - QualityLabel: "480p", - }, } - type args struct { - quality string + + format2 := Format{ + ItagNo: 2, + Quality: "large", + QualityLabel: "480p", + MimeType: `video/mp4; codecs="avc1.42001E, mp4a.40.2"`, + AudioChannels: 1, + } + + formatStereo := Format{ + ItagNo: 3, + URL: "stereo", + AudioChannels: 2, + } + + list := FormatList{ + format1, + format2, + formatStereo, } + tests := []struct { name string - list FormatList - args args - want *Format + args filter + want []Format }{ { - name: "find by quality, get correct one", - list: list, - args: args{ - quality: "medium", - }, - want: &Format{ - ItagNo: 0, - Quality: "medium", - QualityLabel: "360p", + name: "empty list with quality small", + args: filter{ + Quality: "small", }, }, { - name: "find by quality label, get correct one", - list: list, - args: args{ - quality: "480p", - }, - want: &Format{ - ItagNo: 1, - Quality: "large", - QualityLabel: "480p", + name: "empty list with other itagNo", + args: filter{ + ItagNo: 99, }, }, { - name: "find nothing with quality", - list: list, - args: args{ - quality: "small", + name: "empty list with other mimeType", + args: filter{ + MimeType: "other", }, - want: nil, }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - format := tt.list.FindByQuality(tt.args.quality) - assert.Equal(t, format, tt.want) - }) - } -} - -func TestFormatList_FindByItag(t *testing.T) { - list := []Format{{ - ItagNo: 18, - }, - { - ItagNo: 135, - }, - } - type args struct { - itagNo int - } - tests := []struct { - name string - list FormatList - args args - want *Format - }{ { - name: "find itag 18", - list: list, - args: args{ - itagNo: 18, - }, - want: &Format{ - ItagNo: 18, + name: "empty list with other audioChannels", + args: filter{ + AudioChannels: 7, }, }, { - name: "find itag 135", - list: list, - args: args{ - itagNo: 135, - }, - want: &Format{ - ItagNo: 135, + name: "audioChannels stereo", + args: filter{ + AudioChannels: formatStereo.AudioChannels, }, + want: []Format{formatStereo}, }, { - name: "find nothing", - list: list, - args: args{ - itagNo: 9999, + name: "find by medium quality", + args: filter{ + Quality: "medium", }, - want: nil, + want: []Format{format1}, }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - format := tt.list.FindByItag(tt.args.itagNo) - assert.Equal(t, format, tt.want) - }) - } -} - -func TestFormatList_Type(t *testing.T) { - list := []Format{{ - MimeType: "video/mp4; codecs=\"avc1.42001E, mp4a.40.2\"", - }, - } - type args struct { - mimeType string - } - tests := []struct { - name string - list FormatList - args args - want FormatList - }{ { - name: "find video", - list: list, - args: args{ - mimeType: "video/mp4; codecs=\"avc1.42001E, mp4a.40.2\"", + name: "find by 480p", + args: filter{ + Quality: "480p", }, - want: []Format{{ - MimeType: "video/mp4; codecs=\"avc1.42001E, mp4a.40.2\"", - }}, + want: []Format{format2}, }, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - format := tt.list.Type("video") - assert.Equal(t, format, tt.want) + formats := list.Filter(tt.args) + + if tt.want == nil { + assert.Empty(t, formats) + } else { + assert.Equal(t, tt.want, []Format(formats)) + } }) } } func TestFormatList_Sort(t *testing.T) { + t.Parallel() + list := FormatList{ {Width: 512}, {Width: 768, MimeType: "mp4"}, @@ -170,41 +144,3 @@ func TestFormatList_Sort(t *testing.T) { {Width: 512}, }, list) } - -func TestFormatList_WithAudioChannels(t *testing.T) { - list := []Format{ - { - AudioChannels: 0, - Quality: "medium", - QualityLabel: "360p", - }, - { - AudioChannels: 1, - Quality: "large", - QualityLabel: "480p", - }, - } - tests := []struct { - name string - list FormatList - want FormatList - }{ - { - name: "find all formats with Audio Channels", - list: list, - want: []Format{ - { - AudioChannels: 1, - Quality: "large", - QualityLabel: "480p", - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - format := tt.list.WithAudioChannels() - assert.Equal(t, format, tt.want) - }) - } -} diff --git a/go.mod b/go.mod index 13546b44..956a018f 100644 --- a/go.mod +++ b/go.mod @@ -1,47 +1,42 @@ module github.com/kkdai/youtube/v2 -go 1.21 +go 1.26 require ( github.com/bitly/go-simplejson v0.5.1 - github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d + github.com/dop251/goja v0.0.0-20260311135729-065cd970411c github.com/mitchellh/go-homedir v1.1.0 github.com/olekukonko/tablewriter v0.0.5 - github.com/spf13/cobra v1.8.0 - github.com/spf13/pflag v1.0.5 - github.com/spf13/viper v1.17.0 - github.com/stretchr/testify v1.8.4 + github.com/spf13/cobra v1.10.2 + github.com/spf13/pflag v1.0.10 + github.com/spf13/viper v1.21.0 + github.com/stretchr/testify v1.11.1 github.com/vbauerster/mpb/v5 v5.4.0 - golang.org/x/net v0.17.0 + golang.org/x/net v0.52.0 ) require ( github.com/VividCortex/ewma v1.2.0 // indirect github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/dlclark/regexp2 v1.10.0 // indirect - github.com/fsnotify/fsnotify v1.7.0 // indirect - github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect - github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a // indirect - github.com/hashicorp/hcl v1.0.0 // indirect + github.com/dlclark/regexp2 v1.11.5 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect + github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/magiconair/properties v1.8.7 // indirect - github.com/mattn/go-runewidth v0.0.15 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/pelletier/go-toml/v2 v2.1.0 // indirect + github.com/mattn/go-runewidth v0.0.21 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/rivo/uniseg v0.4.4 // indirect - github.com/rogpeppe/go-internal v1.11.0 // indirect - github.com/sagikazarmark/locafero v0.3.0 // indirect - github.com/sagikazarmark/slog-shim v0.1.0 // indirect - github.com/sourcegraph/conc v0.3.0 // indirect - github.com/spf13/afero v1.10.0 // indirect - github.com/spf13/cast v1.5.1 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/sagikazarmark/locafero v0.12.0 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect - go.uber.org/multierr v1.11.0 // indirect - golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect - golang.org/x/sys v0.14.0 // indirect - golang.org/x/text v0.14.0 // indirect - gopkg.in/ini.v1 v1.67.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/text v0.35.0 // indirect + gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 0e2e27d3..642348c3 100644 --- a/go.sum +++ b/go.sum @@ -1,43 +1,5 @@ -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.44.3/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.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= -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/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= -cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= -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/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= +github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA= github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= @@ -45,514 +7,79 @@ github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpH github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= github.com/bitly/go-simplejson v0.5.1 h1:xgwPbetQScXt1gh9BmoJ6j9JMr3TElvuIyjR8pgdoow= github.com/bitly/go-simplejson v0.5.1/go.mod h1:YOPVLzCfwK14b4Sff3oP1AmGhI9T9Vsg84etUnlyp+Q= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/chzyer/test v0.0.0-20210722231415-061457976a23/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/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= -github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0= -github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= -github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d h1:wi6jN5LVt/ljaBG4ue79Ekzb12QfJ52L9Q98tl8SWhw= -github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4= -github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y= -github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM= -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/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= -github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -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-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= -github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= -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/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/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.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -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-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg= -github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a h1:fEBsGL/sjAuJrgah5XqmmYsTLzJp/TO9Lhy39gkverk= -github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= -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/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/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= -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/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/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= +github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= +github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dop251/goja v0.0.0-20260311135729-065cd970411c h1:OcLmPfx1T1RmZVHHFwWMPaZDdRf0DBMZOFMVWJa7Pdk= +github.com/dop251/goja v0.0.0-20260311135729-065cd970411c/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TCYl6lbukKPc7b5x0n1s6Q= +github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc h1:VBbFa1lDYWEeV5FZKUiYKYT0VxCp9twUmmaq9eb8sXw= +github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -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/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= -github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= -github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w= +github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= -github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= -github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= -github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= -github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sagikazarmark/locafero v0.3.0 h1:zT7VEGWC2DTflmccN/5T1etyKvxSxpHsjb9cJvm4SvQ= -github.com/sagikazarmark/locafero v0.3.0/go.mod h1:w+v7UsPNFwzF1cHuOajOOzoq4U7v/ig1mpRjqV+Bu1U= -github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= -github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= -github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= -github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= -github.com/spf13/afero v1.10.0 h1:EaGW2JJh15aKOejeuJ+wpFSHnbd7GE6Wvp3TsNhb6LY= -github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= -github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= -github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= -github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= -github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= -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.17.0 h1:I5txKw7MJasPL/BrfkbA0Jyo/oELqVmux4pR/UxOMfI= -github.com/spf13/viper v1.17.0/go.mod h1:BmMMMLQXSbcHK6KAOiFLz0l5JHrU89OdIRHvsk0+yVI= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -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.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= +github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/vbauerster/mpb/v5 v5.4.0 h1:n8JPunifvQvh6P1D1HAl2Ur9YcmKT1tpoUuiea5mlmg= github.com/vbauerster/mpb/v5 v5.4.0/go.mod h1:fi4wVo7BVQ22QcvFObm+VwliQXlV1eBT8JDaKXR4JGI= -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.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -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.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= -go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -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/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= -golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= -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/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.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -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-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-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -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/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-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/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-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-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-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-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= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= golang.org/x/sys v0.0.0-20201218084310-7d0127a74742/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-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= -golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -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.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -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-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-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-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-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-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -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/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-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-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -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.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/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= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/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/itag_test.go b/itag_test.go index bb919ce2..2c208caf 100644 --- a/itag_test.go +++ b/itag_test.go @@ -14,5 +14,5 @@ func TestYoutube_GetItagInfo(t *testing.T) { url := "https://www.youtube.com/watch?v=rFejpH_tAHM" video, err := client.GetVideo(url) require.NoError(err) - require.GreaterOrEqual(len(video.Formats), 24) + require.GreaterOrEqual(len(video.Formats), 8) } diff --git a/player_parse.go b/player_parse.go index 7790eeb2..8d482b4f 100644 --- a/player_parse.go +++ b/player_parse.go @@ -13,7 +13,7 @@ import ( type playerConfig []byte -var basejsPattern = regexp.MustCompile(`(/s/player/\w+/player_ias.vflset/\w+/base.js)`) +var basejsPattern = regexp.MustCompile(`(/s/player/[A-Za-z0-9_-]+/[A-Za-z0-9._/-]*/base\.js)`) func (c *Client) getPlayerConfig(ctx context.Context, videoID string) (playerConfig, error) { embedURL := fmt.Sprintf("https://youtube.com/embed/%s?hl=en", videoID) diff --git a/playlist.go b/playlist.go index 60462f56..12c11ad1 100644 --- a/playlist.go +++ b/playlist.go @@ -145,7 +145,7 @@ func (p *Playlist) parsePlaylistInfo(ctx context.Context, client *Client, body [ for continuation != "" { data := prepareInnertubePlaylistData(continuation, true, *client.client) - body, err := client.httpPostBodyBytes(ctx, "https://www.youtube.com/youtubei/v1/browse?key="+client.client.key, data) + body, err := client.httpPostBodyBytes(ctx, "https://www.youtube.com/youtubei/v1/browse?key="+client.client.Key, data) if err != nil { return err } @@ -229,10 +229,10 @@ type videosJSONExtractor struct { } func (vje videosJSONExtractor) PlaylistEntry() *PlaylistEntry { - ds, err := strconv.Atoi(vje.Renderer.Duration) - if err != nil { - panic("invalid video duration: " + vje.Renderer.Duration) - } + // A live, premiere, or unavailable (private/deleted) playlist entry has no + // lengthSeconds. Parse leniently (0 when absent) rather than panicking, so a + // single such entry doesn't abort parsing of the whole playlist. + ds, _ := strconv.Atoi(vje.Renderer.Duration) return &PlaylistEntry{ ID: vje.Renderer.ID, Title: vje.Renderer.Title.String(), diff --git a/protobuilder.go b/protobuilder.go new file mode 100644 index 00000000..cec88e78 --- /dev/null +++ b/protobuilder.go @@ -0,0 +1,73 @@ +package youtube + +import ( + "bytes" + "encoding/base64" + "net/url" +) + +type ProtoBuilder struct { + byteBuffer bytes.Buffer +} + +func (pb *ProtoBuilder) ToBytes() []byte { + return pb.byteBuffer.Bytes() +} + +func (pb *ProtoBuilder) ToURLEncodedBase64() string { + b64 := base64.URLEncoding.EncodeToString(pb.ToBytes()) + return url.QueryEscape(b64) +} + +func (pb *ProtoBuilder) writeVarint(val int64) error { + if val == 0 { + _, err := pb.byteBuffer.Write([]byte{0}) + return err + } + for { + b := byte(val & 0x7F) + val >>= 7 + if val != 0 { + b |= 0x80 + } + _, err := pb.byteBuffer.Write([]byte{b}) + if err != nil { + return err + } + if val == 0 { + break + } + } + return nil +} + +func (pb *ProtoBuilder) field(field int, wireType byte) error { + val := int64(field<<3) | int64(wireType&0x07) + return pb.writeVarint(val) +} + +func (pb *ProtoBuilder) Varint(field int, val int64) error { + err := pb.field(field, 0) + if err != nil { + return err + } + return pb.writeVarint(val) +} + +func (pb *ProtoBuilder) String(field int, stringVal string) error { + strBts := []byte(stringVal) + return pb.Bytes(field, strBts) +} + +func (pb *ProtoBuilder) Bytes(field int, bytesVal []byte) error { + if err := pb.field(field, 2); err != nil { + return err + } + + if err := pb.writeVarint(int64(len(bytesVal))); err != nil { + return err + } + + _, err := pb.byteBuffer.Write(bytesVal) + return err +} diff --git a/protobuilder_test.go b/protobuilder_test.go new file mode 100644 index 00000000..3ea921e6 --- /dev/null +++ b/protobuilder_test.go @@ -0,0 +1,18 @@ +package youtube + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestProtoBuilder(t *testing.T) { + var pb ProtoBuilder + + pb.Varint(1, 128) + pb.Varint(2, 1234567890) + pb.Varint(3, 1234567890123456789) + pb.String(4, "Hello") + pb.Bytes(5, []byte{1, 2, 3}) + assert.Equal(t, "CIABENKF2MwEGJWCpu_HnoSRESIFSGVsbG8qAwECAw%3D%3D", pb.ToURLEncodedBase64()) +} diff --git a/response_data.go b/response_data.go index d0331743..154a5728 100644 --- a/response_data.go +++ b/response_data.go @@ -117,6 +117,20 @@ type Format struct { Start string `json:"start"` End string `json:"end"` } `json:"indexRange"` + + // AudioTrack is only available for videos with multiple audio track languages + AudioTrack *struct { + DisplayName string `json:"displayName"` + ID string `json:"id"` + AudioIsDefault bool `json:"audioIsDefault"` + } +} + +func (f *Format) LanguageDisplayName() string { + if f.AudioTrack == nil { + return "" + } + return f.AudioTrack.DisplayName } type Thumbnails []Thumbnail diff --git a/transcript.go b/transcript.go index 84571f45..18724a53 100644 --- a/transcript.go +++ b/transcript.go @@ -47,22 +47,22 @@ func (vt VideoTranscript) String() string { // // Not all videos have transcripts, only relatively new videos. // If transcripts are disabled or not available, ErrTranscriptDisabled is returned. -func (c *Client) GetTranscript(video *Video) (VideoTranscript, error) { - return c.GetTranscriptCtx(context.Background(), video) +func (c *Client) GetTranscript(video *Video, lang string) (VideoTranscript, error) { + return c.GetTranscriptCtx(context.Background(), video, lang) } // GetTranscriptCtx fetches the video transcript if available. // // Not all videos have transcripts, only relatively new videos. // If transcripts are disabled or not available, ErrTranscriptDisabled is returned. -func (c *Client) GetTranscriptCtx(ctx context.Context, video *Video) (VideoTranscript, error) { +func (c *Client) GetTranscriptCtx(ctx context.Context, video *Video, lang string) (VideoTranscript, error) { c.assureClient() if video == nil || video.ID == "" { return nil, fmt.Errorf("no video provided") } - body, err := c.transcriptDataByInnertube(ctx, video.ID) + body, err := c.transcriptDataByInnertube(ctx, video.ID, lang) if err != nil { return nil, err } diff --git a/transcript_test.go b/transcript_test.go index 5d41a01a..965546fb 100644 --- a/transcript_test.go +++ b/transcript_test.go @@ -10,7 +10,29 @@ import ( func TestTranscript(t *testing.T) { video := &Video{ID: "9_MbW9FK1fA"} - transcript, err := testClient.GetTranscript(video) + transcript, err := testClient.GetTranscript(video, "en") + require.NoError(t, err, "get transcript") + require.Greater(t, len(transcript), 0, "no transcript segments found") + + for i, segment := range transcript { + index := strconv.Itoa(i) + + require.NotEmpty(t, segment.Text, "text "+index) + require.NotEmpty(t, segment.Duration, "duration "+index) + require.NotEmpty(t, segment.OffsetText, "offset "+index) + + if i != 0 { + require.NotEmpty(t, segment.StartMs, "startMs "+index) + } + } + + t.Log(transcript.String()) +} + +func TestTranscriptOtherLanguage(t *testing.T) { + video := &Video{ID: "AXwDvYh2-uk"} + + transcript, err := testClient.GetTranscript(video, "id") require.NoError(t, err, "get transcript") require.Greater(t, len(transcript), 0, "no transcript segments found") diff --git a/video.go b/video.go index cbb15208..694172f7 100644 --- a/video.go +++ b/video.go @@ -52,7 +52,7 @@ var playerResponsePattern = regexp.MustCompile(`var ytInitialPlayerResponse\s*=\ func (v *Video) parseVideoPage(body []byte) error { initialPlayerResponse := playerResponsePattern.FindSubmatch(body) - if initialPlayerResponse == nil || len(initialPlayerResponse) < 2 { + if len(initialPlayerResponse) < 2 { return errors.New("no ytInitialPlayerResponse found in the server's answer") } @@ -116,7 +116,10 @@ func (v *Video) extractDataFromPlayerResponse(prData playerResponseData) error { } if str := prData.Microformat.PlayerMicroformatRenderer.PublishDate; str != "" { - v.PublishDate, _ = time.Parse(dateFormat, str) + publishDate, err := time.Parse(time.RFC3339, str) + if err == nil { + v.PublishDate = publishDate.UTC() + } } if profileURL, err := url.Parse(prData.Microformat.PlayerMicroformatRenderer.OwnerProfileURL); err == nil && len(profileURL.Path) > 1 { diff --git a/video_test.go b/video_test.go index f16163b8..e32a0ed9 100644 --- a/video_test.go +++ b/video_test.go @@ -16,8 +16,8 @@ func ExampleClient_GetStream() { // Typically youtube only provides separate streams for video and audio. // If you want audio and video combined, take a look a the downloader package. - format := video.Formats.FindByQuality("medium") - reader, _, err := testClient.GetStream(video, format) + formats := video.Formats.Quality("medium") + reader, _, err := testClient.GetStream(video, &formats[0]) if err != nil { panic(err) } @@ -27,20 +27,42 @@ func ExampleClient_GetStream() { reader.Close() } -func TestSimpleTest(t *testing.T) { +func TestExtractVideoId(t *testing.T) { + testcases := []struct { + input string + expectedID string + }{ + { + input: "https://www.youtube.com/watch?v=9_MbW9FK1fA", + expectedID: "9_MbW9FK1fA", + }, + { + input: "https://www.youtube.com/watch?v=-D2IEZbn5Xs&list=PLQUru4nFApg_ocT-XYXFg50_l4V2BRtwi&index=15", + expectedID: "-D2IEZbn5Xs", + }, + } + + for _, tc := range testcases { + t.Run(tc.input, func(t *testing.T) { + id, err := ExtractVideoID(tc.input) + require.NoError(t, err) + require.Equal(t, tc.expectedID, id) + }) + } + +} +func TestSimpleTest(t *testing.T) { video, err := testClient.GetVideo("https://www.youtube.com/watch?v=9_MbW9FK1fA") require.NoError(t, err, "get body") - _, err = testClient.GetTranscript(video) - require.NoError(t, err, "get transcript") - // Typically youtube only provides separate streams for video and audio. // If you want audio and video combined, take a look a the downloader package. - format := video.Formats.FindByQuality("hd1080") + formats := video.Formats.Quality("hd1080") + require.NotEmpty(t, formats) start := time.Now() - reader, _, err := testClient.GetStream(video, format) + reader, _, err := testClient.GetStream(video, &formats[0]) require.NoError(t, err, "get stream") t.Log("Duration Milliseconds: ", time.Since(start).Milliseconds()) @@ -53,7 +75,6 @@ func TestSimpleTest(t *testing.T) { } func TestDownload_Regular(t *testing.T) { - testcases := []struct { name string url string @@ -111,15 +132,13 @@ func TestDownload_Regular(t *testing.T) { video, err := testClient.GetVideo(tc.url) require.NoError(err) - var format *Format + formats := video.Formats if tc.itagNo > 0 { - format = video.Formats.FindByItag(tc.itagNo) - require.NotNil(format) - } else { - format = &video.Formats[0] + formats = formats.Itag(tc.itagNo) + require.NotEmpty(formats) } - url, err := testClient.GetStreamURL(video, format) + url, err := testClient.GetStreamURL(video, &video.Formats[0]) require.NoError(err) require.NotEmpty(url) }) @@ -139,7 +158,7 @@ func TestDownload_WhenPlayabilityStatusIsNotOK(t *testing.T) { }, { issue: "issue#59", - videoID: "nINQjT7Zr9w", + videoID: "yZIXLfi8CZQ", err: ErrVideoPrivate.Error(), }, } @@ -158,3 +177,22 @@ func TestDownload_SensitiveContent(t *testing.T) { _, err := testClient.GetVideo("MS91knuzoOA") require.EqualError(t, err, "can't bypass age restriction: embedding of this video has been disabled") } + +func TestParse_PublishDate(t *testing.T) { + testcases := []struct { + videoID string + publishDate time.Time + }{ + {"eRqCe_VHs6M", time.Date(2020, time.July, 11, 13, 29, 21, 0, time.UTC)}, + } + + for _, tc := range testcases { + t.Run(tc.videoID, func(t *testing.T) { + got, err := testClient.GetVideo(tc.videoID) + if err != nil { + t.Fatalf("video parsing failed") + } + require.Equal(t, tc.publishDate, got.PublishDate) + }) + } +}