diff --git a/.github/workflows/go.yaml b/.github/workflows/go.yaml index 6f99b191..7deb8194 100644 --- a/.github/workflows/go.yaml +++ b/.github/workflows/go.yaml @@ -9,7 +9,7 @@ jobs: strategy: matrix: platform: [ubuntu-22.04] - go-version: [1.21.x, 1.20.x] + go-version: [1.21.x, 1.x] runs-on: ${{ matrix.platform }} name: Linters (Static Analysis) for Go steps: @@ -27,7 +27,7 @@ jobs: strategy: matrix: platform: [ubuntu-22.04] - go-version: [1.21.x, 1.20.x] + go-version: [1.21.x, 1.x] runs-on: ${{ matrix.platform }} name: integration tests env: diff --git a/.github/workflows/schedule.yaml b/.github/workflows/schedule.yaml index dbb368c2..d3752da1 100644 --- a/.github/workflows/schedule.yaml +++ b/.github/workflows/schedule.yaml @@ -9,7 +9,7 @@ jobs: strategy: matrix: platform: [ubuntu-22.04] - go-version: [1.20.x] + go-version: [1.21.x] runs-on: ${{ matrix.platform }} name: integration tests env: diff --git a/Makefile b/Makefile index c6b24812..02b6aedf 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,5 @@ FILES_TO_FMT ?= $(shell find . -path ./vendor -prune -o -name '*.go' -print) +LOGLEVEL ?= debug ## help: Show makefile commands .PHONY: help @@ -38,14 +39,14 @@ format: ## test-unit: Run all Youtube Go unit tests .PHONY: test-unit test-unit: - go test -v -cover ./... + LOGLEVEL=${LOGLEVEL} go test -v -cover ./... ## test-integration: Run all Youtube Go integration tests .PHONY: test-integration test-integration: mkdir -p output rm -f output/* - ARTIFACTS=output go test -race -covermode=atomic -coverprofile=coverage.out -tags=integration ./... + LOGLEVEL=${LOGLEVEL} ARTIFACTS=output go test -v -race -covermode=atomic -coverprofile=coverage.out -tags=integration ./... .PHONY: coverage.out coverage.out: diff --git a/artifacts.go b/artifacts.go index 0b0ca567..2f8b3990 100644 --- a/artifacts.go +++ b/artifacts.go @@ -1,7 +1,7 @@ package youtube import ( - "log" + "log/slog" "os" "path/filepath" ) @@ -13,15 +13,17 @@ func writeArtifact(name string, content []byte) { // Ensure folder exists err := os.MkdirAll(artifactsFolder, os.ModePerm) if err != nil { - log.Printf("unable to create artifacts folder %s: %s", artifactsFolder, err) + slog.Error("unable to create artifacts folder", "path", artifactsFolder, "error", err) return } path := filepath.Join(artifactsFolder, name) err = os.WriteFile(path, content, 0600) + + log := slog.With("path", path) if err != nil { - log.Printf("unable to write artifact %s: %v", path, err) + log.Error("unable to write artifact", "error", err) } else { - log.Println("artifact created:", path) + log.Debug("artifact created") } } diff --git a/client.go b/client.go index 2d62c864..428724a9 100644 --- a/client.go +++ b/client.go @@ -7,12 +7,13 @@ import ( "errors" "fmt" "io" - "log" "math/rand" "net/http" "net/url" "strconv" "sync/atomic" + + "log/slog" ) const ( @@ -32,9 +33,6 @@ var DefaultClient = AndroidClient // Client offers methods to download video metadata and video streams. type Client struct { - // Debug enables debugging output through log package - Debug bool - // HTTPClient can be used to set a custom HTTP client. // If not set, http.DefaultClient will be used HTTPClient *http.Client @@ -484,10 +482,6 @@ func (c *Client) httpDo(req *http.Request) (*http.Response, error) { client = http.DefaultClient } - if c.Debug { - log.Println(req.Method, req.URL) - } - req.Header.Set("User-Agent", c.client.userAgent) req.Header.Set("Origin", "https://youtube.com") req.Header.Set("Sec-Fetch-Mode", "navigate") @@ -505,8 +499,12 @@ func (c *Client) httpDo(req *http.Request) (*http.Response, error) { res, err := client.Do(req) - if c.Debug && res != nil { - log.Println(res.Status) + log := slog.With("method", req.Method, "url", req.URL) + + if err != nil { + log.Debug("HTTP request failed", "error", err) + } else { + log.Debug("HTTP request succeeded", "status", res.Status) } return res, err diff --git a/client_test.go b/client_test.go index b50e2d09..391ccc5c 100644 --- a/client_test.go +++ b/client_test.go @@ -10,15 +10,15 @@ import ( "golang.org/x/net/context" ) -var testClient = Client{Debug: true} -var testWebClient = Client{Debug: true, client: &WebClient} - const ( dwlURL string = "https://www.youtube.com/watch?v=rFejpH_tAHM" streamURL string = "https://www.youtube.com/watch?v=a9LDPn-MO4I" errURL string = "https://www.youtube.com/watch?v=I8oGsuQ" ) +var testClient = Client{} +var testWebClient = Client{client: &WebClient} + func TestParseVideo(t *testing.T) { video, err := testClient.GetVideo(dwlURL) assert.NoError(t, err) @@ -162,7 +162,6 @@ func TestGetStream(t *testing.T) { // Create testclient to enforce re-using of routines testClient := Client{ - Debug: true, MaxRoutines: 10, ChunkSize: int64(expectedSize) / 11, } diff --git a/cmd/youtubedr/downloader.go b/cmd/youtubedr/downloader.go index 3a478940..697f8280 100644 --- a/cmd/youtubedr/downloader.go +++ b/cmd/youtubedr/downloader.go @@ -4,7 +4,6 @@ import ( "crypto/tls" "errors" "fmt" - "log" "net" "net/http" "net/url" @@ -54,8 +53,10 @@ func getDownloader() *ytdl.Downloader { }).DialContext, } + youtube.SetLogLevel(logLevel) + if insecureSkipVerify { - log.Println("Skip server certificate verification") + youtube.Logger.Info("Skip server certificate verification") httpTransport.TLSClientConfig = &tls.Config{ InsecureSkipVerify: true, } @@ -64,7 +65,6 @@ func getDownloader() *ytdl.Downloader { downloader = &ytdl.Downloader{ OutputDir: outputDir, } - downloader.Client.Debug = verbose downloader.HTTPClient = &http.Client{Transport: httpTransport} return downloader diff --git a/cmd/youtubedr/root.go b/cmd/youtubedr/root.go index 1e432e2a..c2d4bc8e 100644 --- a/cmd/youtubedr/root.go +++ b/cmd/youtubedr/root.go @@ -2,8 +2,6 @@ package main import ( "fmt" - "io" - "log" "os" homedir "github.com/mitchellh/go-homedir" @@ -12,8 +10,8 @@ import ( ) var ( - cfgFile string - verbose bool + cfgFile string + logLevel string ) // rootCmd represents the base command when called without any subcommands @@ -31,18 +29,13 @@ Use the HTTP_PROXY environment variable to set a HTTP or SOCSK5 proxy. The proxy func init() { cobra.OnInitialize(initConfig) - cobra.OnInitialize(func() { - if !verbose { - log.SetOutput(io.Discard) - } - }) // Here you will define your flags and configuration settings. // Cobra supports persistent flags, which, if defined here, // will be global for your application. rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.youtubedr.yaml)") - rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose output") + rootCmd.PersistentFlags().StringVar(&logLevel, "info", "log-level", "Set log level (error/warn/info/debug)") rootCmd.PersistentFlags().BoolVar(&insecureSkipVerify, "insecure", false, "Skip TLS server certificate verification") } diff --git a/decipher.go b/decipher.go index a8ce33da..3dd4c1c2 100644 --- a/decipher.go +++ b/decipher.go @@ -5,7 +5,6 @@ import ( "context" "errors" "fmt" - "log" "net/url" "regexp" "strconv" @@ -76,17 +75,18 @@ func (c *Client) unThrottle(ctx context.Context, videoID string, urlString strin func (c *Client) decryptNParam(config playerConfig, query url.Values) (url.Values, error) { // decrypt n-parameter nSig := query.Get("v") + log := Logger.With("n", nSig) + if nSig != "" { nDecoded, err := config.decodeNsig(nSig) if err != nil { return nil, fmt.Errorf("unable to decode nSig: %w", err) } query.Set("v", nDecoded) + log = log.With("decoded", nDecoded) } - if c.Debug { - log.Printf("[nParam] n: %s; nDecoded: %s\nQuery: %v\n", nSig, query.Get("v"), query) - } + log.Debug("nParam") return query, nil } diff --git a/downloader/downloader.go b/downloader/downloader.go index 748de705..59491600 100644 --- a/downloader/downloader.go +++ b/downloader/downloader.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "io" - "log" "os" "os/exec" "path/filepath" @@ -38,7 +37,12 @@ func (dl *Downloader) getOutputFile(v *youtube.Video, format *youtube.Format, ou // Download : Starting download video by arguments. func (dl *Downloader) Download(ctx context.Context, v *youtube.Video, format *youtube.Format, outputFile string) error { - dl.logf("Video '%s' - Quality '%s' - Codec '%s'", v.Title, format.QualityLabel, format.MimeType) + youtube.Logger.Info( + "Downloading video", + "id", v.ID, + "quality", format.Quality, + "mimeType", format.MimeType, + ) destFile, err := dl.getOutputFile(v, format, outputFile) if err != nil { return err @@ -51,7 +55,6 @@ func (dl *Downloader) Download(ctx context.Context, v *youtube.Video, format *yo } defer out.Close() - dl.logf("Download to file=%s", destFile) return dl.videoDLWorker(ctx, out, v, format) } @@ -62,7 +65,15 @@ func (dl *Downloader) DownloadComposite(ctx context.Context, outputFile string, return err1 } - dl.logf("Video '%s' - Quality '%s' - Video Codec '%s' - Audio Codec '%s'", v.Title, videoFormat.QualityLabel, videoFormat.MimeType, audioFormat.MimeType) + log := youtube.Logger.With("id", v.ID) + + log.Info( + "Downloading composite video", + "videoQuality", videoFormat.QualityLabel, + "videoMimeType", videoFormat.MimeType, + "audioMimeType", audioFormat.MimeType, + ) + destFile, err := dl.getOutputFile(v, videoFormat, outputFile) if err != nil { return err @@ -83,13 +94,13 @@ func (dl *Downloader) DownloadComposite(ctx context.Context, outputFile string, } defer os.Remove(audioFile.Name()) - dl.logf("Downloading video file...") + log.Debug("Downloading video file...") err = dl.videoDLWorker(ctx, videoFile, v, videoFormat) if err != nil { return err } - dl.logf("Downloading audio file...") + log.Debug("Downloading audio file...") err = dl.videoDLWorker(ctx, audioFile, v, audioFormat) if err != nil { return err @@ -106,7 +117,7 @@ func (dl *Downloader) DownloadComposite(ctx context.Context, outputFile string, ) ffmpegVersionCmd.Stderr = os.Stderr ffmpegVersionCmd.Stdout = os.Stdout - dl.logf("merging video and audio to %s", destFile) + log.Info("merging video and audio", "output", destFile) return ffmpegVersionCmd.Run() } @@ -184,9 +195,3 @@ func (dl *Downloader) videoDLWorker(ctx context.Context, out *os.File, video *yo progress.Wait() return nil } - -func (dl *Downloader) logf(format string, v ...interface{}) { - if dl.Debug { - log.Printf(format, v...) - } -} diff --git a/downloader/downloader_test.go b/downloader/downloader_test.go index 84910715..367e897f 100644 --- a/downloader/downloader_test.go +++ b/downloader/downloader_test.go @@ -2,7 +2,6 @@ package downloader import ( "context" - "log" "os" "testing" "time" @@ -15,7 +14,7 @@ import ( var testDownloader = func() (dl Downloader) { dl.OutputDir = "download_test" - dl.Debug = true + return }() @@ -23,7 +22,7 @@ func TestMain(m *testing.M) { exitCode := m.Run() // the following code doesn't work under debugger, please delete download files manually if err := os.RemoveAll(testDownloader.OutputDir); err != nil { - log.Fatal(err.Error()) + panic(err) } os.Exit(exitCode) } diff --git a/go.mod b/go.mod index a35008a5..56ac8ddc 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/kkdai/youtube/v2 -go 1.20 +go 1.21 require ( github.com/bitly/go-simplejson v0.5.1 diff --git a/go.sum b/go.sum index 1f872b31..d64db542 100644 --- a/go.sum +++ b/go.sum @@ -77,6 +77,7 @@ github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5y 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.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= @@ -121,6 +122,7 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ 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= @@ -159,6 +161,7 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN 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= diff --git a/logger.go b/logger.go new file mode 100644 index 00000000..06a6b1c7 --- /dev/null +++ b/logger.go @@ -0,0 +1,27 @@ +package youtube + +import ( + "fmt" + "log/slog" + "os" +) + +var Logger = getLogger(os.Getenv("LOGLEVEL")) + +func SetLogLevel(value string) { + Logger = getLogger(value) +} + +func getLogger(logLevel string) *slog.Logger { + levelVar := slog.LevelVar{} + + if logLevel != "" { + if err := levelVar.UnmarshalText([]byte(logLevel)); err != nil { + panic(fmt.Sprintf("Invalid log level %s: %v", logLevel, err)) + } + } + + return slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ + Level: levelVar.Level(), + })) +} diff --git a/transcript_test.go b/transcript_test.go index 748a3610..5d41a01a 100644 --- a/transcript_test.go +++ b/transcript_test.go @@ -8,11 +8,9 @@ import ( ) func TestTranscript(t *testing.T) { - client := Client{Debug: true} - video := &Video{ID: "9_MbW9FK1fA"} - transcript, err := client.GetTranscript(video) + transcript, err := testClient.GetTranscript(video) require.NoError(t, err, "get transcript") require.Greater(t, len(transcript), 0, "no transcript segments found") diff --git a/video_test.go b/video_test.go index 9105310e..f16163b8 100644 --- a/video_test.go +++ b/video_test.go @@ -9,9 +9,7 @@ import ( ) func ExampleClient_GetStream() { - client := Client{Debug: true} - - video, err := client.GetVideo("https://www.youtube.com/watch?v=9_MbW9FK1fA") + video, err := testClient.GetVideo("https://www.youtube.com/watch?v=9_MbW9FK1fA") if err != nil { panic(err) } @@ -19,7 +17,7 @@ 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 := client.GetStream(video, format) + reader, _, err := testClient.GetStream(video, format) if err != nil { panic(err) } @@ -30,12 +28,11 @@ func ExampleClient_GetStream() { } func TestSimpleTest(t *testing.T) { - client := Client{Debug: true, ChunkSize: Size10Mb} - video, err := client.GetVideo("https://www.youtube.com/watch?v=9_MbW9FK1fA") + video, err := testClient.GetVideo("https://www.youtube.com/watch?v=9_MbW9FK1fA") require.NoError(t, err, "get body") - _, err = client.GetTranscript(video) + _, err = testClient.GetTranscript(video) require.NoError(t, err, "get transcript") // Typically youtube only provides separate streams for video and audio. @@ -43,7 +40,7 @@ func TestSimpleTest(t *testing.T) { format := video.Formats.FindByQuality("hd1080") start := time.Now() - reader, _, err := client.GetStream(video, format) + reader, _, err := testClient.GetStream(video, format) require.NoError(t, err, "get stream") t.Log("Duration Milliseconds: ", time.Since(start).Milliseconds())