From 940b2c5d318f1e3fdd99ec9a374488689666f686 Mon Sep 17 00:00:00 2001 From: trizen Date: Tue, 8 Sep 2020 22:45:41 +0300 Subject: [PATCH] - Added support for loading cookies from a file. The file must be a "# Netscape HTTP Cookie File"; same format as "youtube-dl" requires. The "cookies.txt" extension can be used for exporting the cookies from the browser. This helps with the "429: Too Many Requests" issue. See also: https://github.com/ytdl-org/youtube-dl#how-do-i-pass-cookies-to-youtube-dl - Also added support for changing the user-agent. --- bin/gtk2-youtube-viewer | 35 ++++++++----- bin/gtk3-youtube-viewer | 36 ++++++++----- bin/youtube-viewer | 34 ++++++++---- lib/WWW/YoutubeViewer.pm | 109 ++++++++++++++++++++++++++++----------- 4 files changed, 145 insertions(+), 69 deletions(-) diff --git a/bin/gtk2-youtube-viewer b/bin/gtk2-youtube-viewer index f47b7879..29d9b8af 100755 --- a/bin/gtk2-youtube-viewer +++ b/bin/gtk2-youtube-viewer @@ -15,7 +15,7 @@ #------------------------------------------------------- # GTK YouTube Viewer # Created on: 12 September 2010 -# Latest edit on: 24 August 2020 +# Latest edit on: 08 September 2020 # https://github.com/trizen/youtube-viewer #------------------------------------------------------- @@ -25,7 +25,7 @@ use 5.016; use warnings; no warnings 'once'; -my $DEVEL; # true in devel mode +my $DEVEL; # true in devel mode use if ($DEVEL = 0), lib => qw(../lib); # devel only use WWW::YoutubeViewer v3.7.8; @@ -216,13 +216,16 @@ my %CONFIG = ( # Others env_proxy => 1, http_proxy => undef, + timeout => undef, + user_agent => undef, + cookie_file => undef, prefer_fork => (($^O eq 'linux') ? 0 : 1), debug => 0, fullscreen => 0, audio_only => 0, tooltips => 1, - tooltip_max_len => 512, # max length of description in tooltips + tooltip_max_len => 512, # max length of description in tooltips thousand_separator => q{,}, downloads_dir => curdir(), @@ -735,6 +738,9 @@ my $yv_obj = WWW::YoutubeViewer->new( hl => $CONFIG{hl}, lwp_env_proxy => $CONFIG{env_proxy}, cache_dir => $CONFIG{cache_dir}, + cookie_file => $CONFIG{cookie_file}, + user_agent => $CONFIG{user_agent}, + timeout => $CONFIG{timeout}, authentication_file => $authentication_file, ); @@ -807,7 +813,8 @@ sub apply_configuration { videoEmbeddable videoLicense publishedAfter publishedBefore regionCode videoCategoryId - debug http_proxy + debug http_proxy user_agent + timeout cookie_file ) ) { @@ -1363,8 +1370,8 @@ $accel->connect(ord('g'), ['control-mask'], ['visible'], \&show_user_favorited_v $accel->connect(ord('m'), ['control-mask'], ['visible'], \&show_videos_from_selected_author); $accel->connect(ord('k'), ['control-mask'], ['visible'], \&show_playlists_from_selected_author); $accel->connect(ord('w'), ['control-mask'], ['visible'], \&show_warnings_window); -$accel->connect(0xffff, ['lock-mask'], ['visible'], \&delete_selected_row); -$accel->connect(0xffc8, ['lock-mask'], ['visible'], \&maximize_unmaximize_mainw); +$accel->connect(0xffff, ['lock-mask'], ['visible'], \&delete_selected_row); +$accel->connect(0xffc8, ['lock-mask'], ['visible'], \&maximize_unmaximize_mainw); $mainw->add_accel_group($accel); # Support for navigating back and forth using the side buttons of the mouse @@ -1393,7 +1400,7 @@ $accel->connect(0xff1b, ['lock-mask'], ['visible'], \&hide_feeds_window); $feeds_window->add_accel_group($accel); $accel = Gtk2::AccelGroup->new; -$accel->connect(0xff1b, ['lock-mask'], ['visible'], \&hide_preferences_window); +$accel->connect(0xff1b, ['lock-mask'], ['visible'], \&hide_preferences_window); $accel->connect(ord('s'), ['control-mask'], ['visible'], \&save_configuration); $prefernces_window->add_accel_group($accel); @@ -1974,8 +1981,8 @@ sub set_youtube_tops { } sub remove_selected_user { - my $selection = $users_treeview->get_selection // return; - my $iter = $selection->get_selected // return; + my $selection = $users_treeview->get_selection // return; + my $iter = $selection->get_selected // return; my $channel_id = $users_liststore->get($iter, 0); delete $channels{$channel_id}; $users_liststore->remove($iter); @@ -2421,7 +2428,7 @@ sub display_results { my $url = $results->{url}; my $info = $results->{results} // {}; - my $items = $info->{items} // []; + my $items = $info->{items} // []; hide_feeds_window(); @@ -3067,10 +3074,10 @@ sub get_options_as_arguments { 'no-interactive' => q{}, 'resolution' => $CONFIG{resolution}, 'download-dir' => quotemeta(rel2abs($CONFIG{downloads_dir})), - 'fullscreen' => $CONFIG{fullscreen} ? q{} : undef, + 'fullscreen' => $CONFIG{fullscreen} ? q{} : undef, 'no-dash' => $CONFIG{dash_support} ? undef : q{}, - 'no-video' => $CONFIG{audio_only} ? q{} : undef, - 'resolution=audio' => $CONFIG{audio_only} ? q{} : undef, + 'no-video' => $CONFIG{audio_only} ? q{} : undef, + 'resolution=audio' => $CONFIG{audio_only} ? q{} : undef, ); while (my ($argv, $value) = each %options) { @@ -3273,7 +3280,7 @@ sub display_comments { my $url = $results->{url}; my $res = $results->{results} // {}; - my $comments = $res->{items} // []; + my $comments = $res->{items} // []; foreach my $comment (@{$comments}) { my $snippet = (($comment->{snippet} // next)->{topLevelComment} // next)->{snippet}; diff --git a/bin/gtk3-youtube-viewer b/bin/gtk3-youtube-viewer index 4e13056e..61fbad87 100755 --- a/bin/gtk3-youtube-viewer +++ b/bin/gtk3-youtube-viewer @@ -15,7 +15,7 @@ #------------------------------------------------------- # GTK YouTube Viewer # Created on: 12 September 2010 -# Latest edit on: 24 August 2020 +# Latest edit on: 08 September 2020 # https://github.com/trizen/youtube-viewer #------------------------------------------------------- @@ -25,7 +25,7 @@ use 5.016; use warnings; no warnings 'once'; -my $DEVEL; # true in devel mode +my $DEVEL; # true in devel mode use if ($DEVEL = 0), lib => qw(../lib); # devel only use WWW::YoutubeViewer v3.7.8; @@ -217,13 +217,16 @@ my %CONFIG = ( # Others env_proxy => 1, http_proxy => undef, + timeout => undef, + user_agent => undef, + cookie_file => undef, prefer_fork => (($^O eq 'linux') ? 0 : 1), debug => 0, fullscreen => 0, audio_only => 0, tooltips => 1, - tooltip_max_len => 512, # max length of description in tooltips + tooltip_max_len => 512, # max length of description in tooltips thousand_separator => q{,}, downloads_dir => curdir(), @@ -806,6 +809,10 @@ my $yv_obj = WWW::YoutubeViewer->new( hl => $CONFIG{hl}, lwp_env_proxy => $CONFIG{env_proxy}, cache_dir => $CONFIG{cache_dir}, + cookie_file => $CONFIG{cookie_file}, + user_agent => $CONFIG{user_agent}, + http_proxy => $CONFIG{http_proxy}, + timeout => $CONFIG{timeout}, authentication_file => $authentication_file, ); @@ -878,7 +885,8 @@ sub apply_configuration { videoEmbeddable videoLicense publishedAfter publishedBefore regionCode videoCategoryId - debug http_proxy + debug http_proxy user_agent + timeout cookie_file ) ) { @@ -1434,8 +1442,8 @@ $accel->connect(ord('g'), ['control-mask'], ['visible'], \&show_user_favorited_v $accel->connect(ord('m'), ['control-mask'], ['visible'], \&show_videos_from_selected_author); $accel->connect(ord('k'), ['control-mask'], ['visible'], \&show_playlists_from_selected_author); $accel->connect(ord('w'), ['control-mask'], ['visible'], \&show_warnings_window); -$accel->connect(0xffff, ['lock-mask'], ['visible'], \&delete_selected_row); -$accel->connect(0xffc8, ['lock-mask'], ['visible'], \&maximize_unmaximize_mainw); +$accel->connect(0xffff, ['lock-mask'], ['visible'], \&delete_selected_row); +$accel->connect(0xffc8, ['lock-mask'], ['visible'], \&maximize_unmaximize_mainw); $mainw->add_accel_group($accel); # Support for navigating back and forth using the side buttons of the mouse @@ -1464,7 +1472,7 @@ $accel->connect(0xff1b, ['lock-mask'], ['visible'], \&hide_feeds_window); $feeds_window->add_accel_group($accel); $accel = Gtk3::AccelGroup->new; -$accel->connect(0xff1b, ['lock-mask'], ['visible'], \&hide_preferences_window); +$accel->connect(0xff1b, ['lock-mask'], ['visible'], \&hide_preferences_window); $accel->connect(ord('s'), ['control-mask'], ['visible'], \&save_configuration); $prefernces_window->add_accel_group($accel); @@ -2077,8 +2085,8 @@ sub set_youtube_tops { } sub remove_selected_user { - my $selection = $users_treeview->get_selection // return; - my $iter = $selection->get_selected // return; + my $selection = $users_treeview->get_selection // return; + my $iter = $selection->get_selected // return; my $channel_id = $users_liststore->get($iter, 0); delete $channels{$channel_id}; $users_liststore->remove($iter); @@ -2572,7 +2580,7 @@ sub display_results { my $url = $results->{url}; my $info = $results->{results} // {}; - my $items = $info->{items} // []; + my $items = $info->{items} // []; hide_feeds_window(); @@ -3223,10 +3231,10 @@ sub get_options_as_arguments { 'no-interactive' => q{}, 'resolution' => $CONFIG{resolution}, 'download-dir' => quotemeta(rel2abs($CONFIG{downloads_dir})), - 'fullscreen' => $CONFIG{fullscreen} ? q{} : undef, + 'fullscreen' => $CONFIG{fullscreen} ? q{} : undef, 'no-dash' => $CONFIG{dash_support} ? undef : q{}, - 'no-video' => $CONFIG{audio_only} ? q{} : undef, - 'resolution=audio' => $CONFIG{audio_only} ? q{} : undef, + 'no-video' => $CONFIG{audio_only} ? q{} : undef, + 'resolution=audio' => $CONFIG{audio_only} ? q{} : undef, ); while (my ($argv, $value) = each %options) { @@ -3431,7 +3439,7 @@ sub display_comments { my $url = $results->{url}; my $res = $results->{results} // {}; - my $comments = $res->{items} // []; + my $comments = $res->{items} // []; foreach my $comment (@{$comments}) { my $snippet = (($comment->{snippet} // next)->{topLevelComment} // next)->{snippet}; diff --git a/bin/youtube-viewer b/bin/youtube-viewer index 3113726a..8c30be6c 100755 --- a/bin/youtube-viewer +++ b/bin/youtube-viewer @@ -15,7 +15,7 @@ #------------------------------------------------------- # youtube-viewer # Created on: 02 June 2010 -# Latest edit on: 24 August 2020 +# Latest edit on: 08 September 2020 # https://github.com/trizen/youtube-viewer #------------------------------------------------------- @@ -71,7 +71,7 @@ use 5.016; use warnings; no warnings 'once'; -my $DEVEL; # true in devel mode +my $DEVEL; # true in devel mode use if ($DEVEL = 0), lib => qw(../lib); # devel mode use WWW::YoutubeViewer v3.7.8; @@ -190,7 +190,7 @@ my %CONFIG = ( video_player_selected => ( $constant{win32} ? 'vlc' - : undef # auto-defined + : undef # auto-defined ), # YouTube options @@ -230,6 +230,9 @@ my %CONFIG = ( # Others autoplay_mode => 0, http_proxy => undef, + cookie_file => undef, + user_agent => undef, + timeout => undef, env_proxy => 1, confirm => 0, debug => 0, @@ -618,6 +621,10 @@ my $yv_obj = WWW::YoutubeViewer->new( config_dir => $config_dir, cache_dir => $opt{cache_dir}, lwp_env_proxy => $opt{env_proxy}, + cookie_file => $opt{cookie_file}, + http_proxy => $opt{http_proxy}, + user_agent => $opt{user_agent}, + timeout => $opt{timeout}, authentication_file => $authentication_file, ); @@ -833,6 +840,8 @@ usage: $execname [options] ([url] | [keywords]) --use-colors! : enable or disable the ANSI colors for text * Other + --cookies=s : file to read cookies from and dump cookie + --user-agent=s : specify a custom user agent --proxy=s : set HTTP(S)/SOCKS proxy: 'proto://domain.tld:port/' If authentication required, use 'proto://user:pass\@domain.tld:port/' @@ -1133,7 +1142,8 @@ sub apply_configuration { publishedAfter publishedBefore safeSearch regionCode debug hl http_proxy page comments_order - subscriptions_order + subscriptions_order user_agent + cookie_file timeout ) ) { @@ -1554,6 +1564,8 @@ sub parse_arguments { 'related-videos|rv=s' => \$opt{related_videos}, 'popular-videos|popular|pv=s' => \$opt{popular_videos}, + 'cookie-file|cookies=s' => \$opt{cookie_file}, + 'user-agent|agent=s' => \$opt{user_agent}, 'http_proxy|http-proxy|proxy=s' => \$opt{http_proxy}, 'catlang|cl|hl=s' => \$opt{hl}, @@ -2548,7 +2560,7 @@ sub print_channels { my $url = $results->{url}; my $info = $results->{results} // {}; - my $channels = $info->{items} // []; + my $channels = $info->{items} // []; foreach my $i (0 .. $#{$channels}) { my $channel = $channels->[$i]; @@ -2662,7 +2674,7 @@ sub print_comments { my $url = $results->{url}; my $info = $results->{results} // {}; - my $comments = $info->{items} // []; + my $comments = $info->{items} // []; my $i = 0; foreach my $comment (@{$comments}) { @@ -2850,7 +2862,7 @@ sub print_playlists { my $url = $results->{url}; my $info = $results->{results} // {}; - my $playlists = $info->{items} // []; + my $playlists = $info->{items} // []; state $info_format = <<"FORMAT"; @@ -3046,7 +3058,7 @@ sub parse_page_number { $page_number = int($total / $per_page); $page_number += 1 if ($total % $per_page != 0); - $page_number = 1 if ($page_number <= 0); + $page_number = 1 if ($page_number <= 0); } } elsif ($page_number =~ /first|beg/) { @@ -3122,7 +3134,7 @@ sub get_streaming_url { resolution => ($opt{novideo} ? 'audio' : $opt{resolution}), hfr => $opt{hfr}, dash => $dash, - dash_mp4_audio => ($opt{novideo} ? 1 : $opt{dash_mp4_audio}), + dash_mp4_audio => ($opt{novideo} ? 1 : $opt{dash_mp4_audio}), dash_segmented => ($opt{download_video} ? 0 : $opt{dash_segmented}), ); @@ -3392,7 +3404,7 @@ sub get_player_command { $MPLAYER{fullscreen} = $opt{fullscreen} ? $opt{video_players}{$opt{video_player_selected}}{fs} // '' : q{}; $MPLAYER{novideo} = $opt{novideo} ? $opt{video_players}{$opt{video_player_selected}}{novideo} // '' : q{}; - $MPLAYER{arguments} = $opt{video_players}{$opt{video_player_selected}}{arg} // q{}; + $MPLAYER{arguments} = $opt{video_players}{$opt{video_player_selected}}{arg} // q{}; my $cmd = join( q{ }, @@ -3647,7 +3659,7 @@ sub print_videos { my $url = $results->{url}; my $info = $results->{results} // {}; - my $videos = $info->{items} // []; + my $videos = $info->{items} // []; foreach my $entry (@$videos) { if ($yv_utils->is_activity($entry)) { diff --git a/lib/WWW/YoutubeViewer.pm b/lib/WWW/YoutubeViewer.pm index 313ec097..dcf75ecc 100644 --- a/lib/WWW/YoutubeViewer.pm +++ b/lib/WWW/YoutubeViewer.pm @@ -41,21 +41,21 @@ my %valid_options = ( # Main options v => {valid => q[], default => 3}, - page => {valid => [qr/^(?!0+\z)\d+\z/], default => 1}, - http_proxy => {valid => [qr{.}], default => undef}, - hl => {valid => [qr/^\w+(?:[\-_]\w+)?\z/], default => undef}, + page => {valid => qr/^(?!0+\z)\d+\z/, default => 1}, + http_proxy => {valid => qr/./, default => undef}, + hl => {valid => qr/^\w+(?:[\-_]\w+)?\z/, default => undef}, maxResults => {valid => [1 .. 50], default => 10}, - topicId => {valid => [qr/^./], default => undef}, + topicId => {valid => qr/./, default => undef}, order => {valid => [qw(relevance date rating viewCount title videoCount)], default => undef}, - publishedAfter => {valid => [qr/^\d+/], default => undef}, - publishedBefore => {valid => [qr/^\d+/], default => undef}, - channelId => {valid => [qr/^[-\w]{2,}\z/], default => undef}, + publishedAfter => {valid => qr/^\d+/, default => undef}, + publishedBefore => {valid => qr/^\d+/, default => undef}, + channelId => {valid => qr/^[-\w]{2,}\z/, default => undef}, channelType => {valid => [qw(any show)], default => undef}, # Video only options videoCaption => {valid => [qw(any closedCaption none)], default => undef}, videoDefinition => {valid => [qw(any high standard)], default => undef}, - videoCategoryId => {valid => [qr/^\d+\z/], default => undef}, + videoCategoryId => {valid => qr/^\d+\z/, default => undef}, videoDimension => {valid => [qw(any 2d 3d)], default => undef}, videoDuration => {valid => [qw(any short medium long)], default => undef}, videoEmbeddable => {valid => [qw(any true)], default => undef}, @@ -64,8 +64,8 @@ my %valid_options = ( eventType => {valid => [qw(completed live upcoming)], default => undef}, chart => {valid => [qw(mostPopular)], default => 'mostPopular'}, - regionCode => {valid => [qr/^[A-Z]{2}\z/i], default => undef}, - relevanceLanguage => {valid => [qr/^[a-z](?:\-\w+)?\z/i], default => undef}, + regionCode => {valid => qr/^[A-Z]{2}\z/i, default => undef}, + relevanceLanguage => {valid => qr/^[a-z](?:\-\w+)?\z/i, default => undef}, safeSearch => {valid => [qw(none moderate strict)], default => undef}, videoType => {valid => [qw(any episode movie)], default => undef}, @@ -73,10 +73,11 @@ my %valid_options = ( subscriptions_order => {valid => [qw(alphabetical relevance unread)], default => undef}, # Misc - debug => {valid => [0 .. 3], default => 0}, - lwp_timeout => {valid => [qr/^\d+\z/], default => 1}, - config_dir => {valid => [qr/^./], default => q{.}}, - cache_dir => {valid => [qr/^./], default => q{.}}, + debug => {valid => [0 .. 3], default => 0}, + timeout => {valid => qr/^\d+\z/, default => 10}, + config_dir => {valid => qr/^./, default => q{.}}, + cache_dir => {valid => qr/^./, default => q{.}}, + cookie_file => {valid => qr/^./, default => undef}, # Booleans lwp_env_proxy => {valid => [1, 0], default => 1}, @@ -85,14 +86,14 @@ my %valid_options = ( prefer_av1 => {valid => [1, 0], default => 0}, # API/OAuth - key => {valid => [qr/^.{15}/], default => undef}, - client_id => {valid => [qr/^.{15}/], default => undef}, - client_secret => {valid => [qr/^.{15}/], default => undef}, - redirect_uri => {valid => [qr/^.{15}/], default => 'urn:ietf:wg:oauth:2.0:oob'}, - access_token => {valid => [qr/^.{15}/], default => undef}, - refresh_token => {valid => [qr/^.{15}/], default => undef}, + key => {valid => qr/^.{15}/, default => undef}, + client_id => {valid => qr/^.{15}/, default => undef}, + client_secret => {valid => qr/^.{15}/, default => undef}, + redirect_uri => {valid => qr/^.{15}/, default => 'urn:ietf:wg:oauth:2.0:oob'}, + access_token => {valid => qr/^.{15}/, default => undef}, + refresh_token => {valid => qr/^.{15}/, default => undef}, - authentication_file => {valid => [qr/^./], default => undef}, + authentication_file => {valid => qr/^./, default => undef}, # No input value allowed feeds_url => {valid => q[], default => 'https://www.googleapis.com/youtube/v3/'}, @@ -103,7 +104,7 @@ my %valid_options = ( #<<< # LWP user agent - lwp_agent => {valid => [qr/^.{5}/], default => 'Mozilla/5.0 (Windows NT 10.0; Win64; gzip; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.0.0 Safari/537.36'}, + user_agent => {valid => qr/^.{5}/, default => 'Mozilla/5.0 (Windows NT 10.0; Win64; gzip; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.0.0 Safari/537.36'}, #>>> ); @@ -112,7 +113,7 @@ sub _our_smartmatch { $value // return 0; - if (ref($arg) eq '') { + if (not ref($arg)) { return ($value eq $arg); } @@ -134,7 +135,7 @@ sub _our_smartmatch { foreach my $key (keys %valid_options) { - if (ref $valid_options{$key}{valid} eq 'ARRAY') { + if (ref($valid_options{$key}{valid})) { # Create the 'set_*' subroutines *{__PACKAGE__ . '::set_' . $key} = sub { @@ -237,10 +238,10 @@ sub set_lwp_useragent { $self->{lwp} = $lwp->new( - cookie_jar => {}, # temporary cookies - timeout => $self->get_lwp_timeout, + cookie_jar => {}, # temporary cookies + timeout => $self->get_timeout, show_progress => $self->get_debug, - agent => $self->get_lwp_agent, + agent => $self->get_user_agent, ssl_opts => {verify_hostname => 1}, @@ -287,6 +288,47 @@ sub set_lwp_useragent { $agent->conn_cache($cache); $agent->proxy(['http', 'https'], $self->get_http_proxy) if defined($self->get_http_proxy); + my $cookie_file = $self->get_cookie_file; + + if (defined($cookie_file) and -f $cookie_file) { + + if ($self->get_debug) { + say STDERR ":: Using cookies from: $cookie_file"; + } + +#<<< + ## LWP Cookies + #~ require HTTP::Cookies; + + #~ my $cookies = HTTP::Cookies->new( + #~ file => $cookie_file, + #~ autosave => 1, + #~ ); +#>>> + + ## Netscape HTTP Cookies + + # Chrome extension: + # https://chrome.google.com/webstore/detail/cookiestxt/njabckikapfpffapmjgojcnbfjonfjfg + + # Firefox extension: + # https://addons.mozilla.org/en-US/firefox/addon/cookies-txt/ + + # See also: + # https://github.com/ytdl-org/youtube-dl#how-do-i-pass-cookies-to-youtube-dl + + require HTTP::Cookies::Netscape; + + my $cookies = HTTP::Cookies::Netscape->new( + hide_cookie2 => 1, + autosave => 1, + file => $cookie_file, + ); + + $cookies->load; + $agent->cookie_jar($cookies); + } + #my $http_proxy = $agent->proxy('http'); #if (defined($http_proxy)) { # $agent->proxy('https', $http_proxy) if (!defined($agent->proxy('https'))); @@ -505,7 +547,7 @@ sub get_invidious_instances { require LWP::UserAgent; - my $lwp = LWP::UserAgent->new(timeout => 10); + my $lwp = LWP::UserAgent->new(timeout => $self->get_timeout); $lwp->show_progress(1) if $self->get_debug; my $resp = $lwp->get("https://instances.invidio.us/instances.json"); @@ -620,8 +662,15 @@ sub _extract_from_ytdl { $self->_ytdl_is_available() || return; - my $json = $self->proxy_stdout('youtube-dl', '--all-formats', '--dump-single-json', - quotemeta("https://www.youtube.com/watch?v=" . $videoID)); + my @ytdl_cmd = ('youtube-dl', '--all-formats', '--dump-single-json'); + + my $cookie_file = $self->get_cookie_file; + + if (defined($cookie_file) and -f $cookie_file) { + push @ytdl_cmd, '--cookies', $cookie_file; + } + + my $json = $self->proxy_stdout(@ytdl_cmd, quotemeta("https://www.youtube.com/watch?v=" . $videoID)); my $ref = $self->parse_json_string($json);