v1ral_ITS

youtube-viewer shell script

Oct 11th, 2018
334
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Bash 121.48 KB | None | 0 0
  1. #!/usr/bin/perl
  2.  
  3. # youtube-viewer Shell Script
  4.  
  5. =head1 NAME
  6.  
  7. youtube-viewer - YouTube from command line.
  8.  
  9. See: youtube-viewer --help
  10.      youtube-viewer --tricks
  11.      youtube-viewer --examples
  12.      youtube-viewer --stdin-help
  13.  
  14. =head1 LICENSE AND COPYRIGHT
  15.  
  16. Copyright 2010-2018 Trizen.
  17.  
  18. This program is free software; you can redistribute it and/or modify it
  19. under the terms of either: the GNU General Public License as published
  20. by the Free Software Foundation; or the Artistic License.
  21.  
  22. This program is distributed in the hope that it will be useful,
  23. but WITHOUT ANY WARRANTY; without even the implied warranty of
  24. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
  25.  
  26. See L<https://dev.perl.org/licenses/> for more information.
  27.  
  28. =cut
  29.  
  30. use utf8;
  31. use 5.016;
  32.  
  33. use warnings;
  34. no warnings 'once';
  35.  
  36. my $DEVEL;    # true in devel mode
  37. use if ($DEVEL = 0), lib => qw(../lib);    # devel mode
  38.  
  39. use WWW::YoutubeViewer v3.5.0;
  40. use WWW::YoutubeViewer::RegularExpressions;
  41.  
  42. use File::Spec::Functions qw(
  43.   catdir
  44.   catfile
  45.   curdir
  46.   path
  47.   rel2abs
  48.   tmpdir
  49.   file_name_is_absolute
  50.   );
  51.  
  52. binmode(STDOUT, ':utf8');
  53.  
  54. my $appname  = 'Youtube Viewer';
  55. my $version  = $WWW::YoutubeViewer::VERSION;
  56. my $execname = 'youtube-viewer';
  57.  
  58. # A better <STDIN> support:
  59. require Term::ReadLine;
  60. my $term = Term::ReadLine->new("$appname $version");
  61.  
  62. # Developer key
  63. my $key = 'aXalQYmzI8gPkMSLyMhpApfMAiU2b23Qz2nE3mq';
  64.  
  65. sub VIDEO_PART () { 'contentDetails,statistics,snippet' }
  66.  
  67. # Options (key=>value) goes here
  68. my %opt;
  69. my $term_width = 80;
  70.  
  71. # Keep track of watched videos by their ID
  72. my %watched_videos;
  73.  
  74. # Unchangeable data goes here
  75. my %constant = (win32 => ($^O eq 'MSWin32' ? 1 : 0));    # doh
  76.  
  77. my $home_dir;
  78. my $xdg_config_home = $ENV{XDG_CONFIG_HOME};
  79.  
  80. if ($xdg_config_home and -d -w $xdg_config_home) {
  81.     require File::Basename;
  82.     $home_dir = File::Basename::dirname($xdg_config_home);
  83.  
  84.     if (not -d -w $home_dir) {
  85.         $home_dir = curdir();
  86.     }
  87. }
  88. else {
  89.     $home_dir =
  90.          $ENV{HOME}
  91.       || $ENV{LOGDIR}
  92.       || ($constant{win32} ? '\Local Settings\Application Data' : ((getpwuid($<))[7] || `echo -n ~`));
  93.  
  94.     if (not -d -w $home_dir) {
  95.         $home_dir = curdir();
  96.     }
  97.  
  98.     $xdg_config_home = catdir($home_dir, '.config');
  99. }
  100.  
  101. # Configuration dir/file
  102. my $config_dir = catdir($xdg_config_home, $execname);
  103. my $config_file         = catfile($config_dir, "$execname.conf");
  104. my $authentication_file = catfile($config_dir, 'reg.dat');
  105. my $history_file        = catfile($config_dir, 'history.txt');
  106.  
  107. if (not -d $config_dir) {
  108.     require File::Path;
  109.     File::Path::make_path($config_dir)
  110.       or warn "[!] Can't create dir '$config_dir': $!";
  111. }
  112.  
  113. sub which_command {
  114.     my ($cmd) = @_;
  115.  
  116.     if (file_name_is_absolute($cmd)) {
  117.         return $cmd;
  118.     }
  119.  
  120.     state $paths = [path()];
  121.     foreach my $path (@{$paths}) {
  122.         if (-e (my $cmd_path = catfile($path, $cmd))) {
  123.             return $cmd_path;
  124.         }
  125.     }
  126.     return;
  127. }
  128.  
  129. # Main configuration
  130. my %CONFIG = (
  131.  
  132.     video_players => {
  133.                       vlc => {
  134.                               cmd     => q{vlc},
  135.                               srt     => q{--sub-file *SUB*},
  136.                               audio   => q{--input-slave *AUDIO*},
  137.                               fs      => q{--fullscreen},
  138.                               arg     => q{--quiet --play-and-exit --no-video-title-show --input-title-format *TITLE*},
  139.                               novideo => q{--intf dummy --novideo},
  140.                              },
  141.                       mpv => {
  142.                               cmd     => q{mpv},
  143.                               srt     => q{--sub-file *SUB*},
  144.                               audio   => q{--audio-file *AUDIO*},
  145.                               fs      => q{--fullscreen},
  146.                               arg     => q{--really-quiet --title *TITLE* --no-ytdl},
  147.                               novideo => q{--no-video},
  148.                              },
  149.                       mplayer => {
  150.                                   cmd     => q{mplayer},
  151.                                   srt     => q{-sub *SUB*},
  152.                                   audio   => q{-audiofile *AUDIO*},
  153.                                   fs      => q{-fs},
  154.                                   arg     => q{-prefer-ipv4 -really-quiet -title *TITLE*},
  155.                                   novideo => q{-novideo},
  156.                                  },
  157.                      },
  158.  
  159.     video_player_selected => (
  160.         $constant{win32}
  161.         ? 'mplayer'
  162.         : undef            # auto-defined later
  163.     ),
  164.  
  165.     combine_multiple_videos => 0,
  166.  
  167.     # YouTube options
  168.     dash_support    => 1,
  169.     dash_mp4_audio  => 1,
  170.     maxResults      => 20,
  171.     resolution      => 'original',
  172.     videoDefinition => undef,
  173.     videoDimension  => undef,
  174.     videoLicense    => undef,
  175.     safeSearch      => undef,
  176.     videoCaption    => undef,
  177.     videoDuration   => undef,
  178.     videoSyndicated => undef,
  179.     publishedBefore => undef,
  180.     publishedAfter  => undef,
  181.     order           => undef,
  182.  
  183.     subscriptions_order => 'relevance',
  184.  
  185.     hl         => 'en_US',
  186.     regionCode => undef,
  187.  
  188.     # URI options
  189.     youtube_video_url => 'https://www.youtube.com/watch?v=%s',
  190.  
  191.     # Subtitle options
  192.     srt_languages => ['en', 'es'],
  193.     captions_dir  => tmpdir(),
  194.     get_captions  => 1,
  195.     auto_captions => 0,
  196.     copy_caption  => 0,
  197.     cache_dir     => undef,          # will be defined later
  198.  
  199.     # Others
  200.     http_proxy           => undef,
  201.     env_proxy            => 1,
  202.     confirm              => 0,
  203.     debug                => 0,
  204.     page                 => 1,
  205.     colors               => $constant{win32} ^ 1,
  206.     clobber              => 0,
  207.     skip_if_exists       => 1,
  208.     prefer_mp4           => 0,
  209.     fat32safe            => $constant{win32},
  210.     fullscreen           => 0,
  211.     results_with_details => 0,
  212.     results_with_colors  => 0,
  213.     results_fixed_width  => 0,
  214.     interactive          => 1,
  215.     get_term_width       => $constant{win32} ^ 1,
  216.     download_with_wget   => undef,                    # auto-defined later
  217.     download_in_parallel => 0,
  218.     thousand_separator   => q{,},
  219.     downloads_dir        => curdir(),
  220.     keep_original_video  => 0,
  221.     download_and_play    => 0,
  222.     autohide_watched     => 0,
  223.     highlight_watched    => 1,
  224.     highlight_color      => 'bold',
  225.     remove_played_file   => 0,
  226.     history              => 0,
  227.     history_limit        => 10_000,
  228.     history_file         => $history_file,
  229.     convert_cmd          => 'ffmpeg -i *IN* *OUT*',
  230.     convert_to           => undef,
  231.  
  232.     custom_layout        => 0,
  233.     custom_layout_format => '*NO*. *TITLE* (*AUTHOR*) (*RATING*) [*TIME*]\n',
  234.  
  235.     ffmpeg_cmd => 'ffmpeg',
  236.     wget_cmd   => 'wget',
  237.  
  238.     merge_into_mkv      => undef,                                                                    # auto-defined later
  239.     merge_into_mkv_args => '-loglevel warning -c:s srt -c:v copy -c:a copy -disposition:s forced',
  240.     merge_with_captions => 1,
  241.  
  242.     video_filename_format => '*FTITLE* - *ID*.*FORMAT*',
  243. );
  244.  
  245. local $SIG{__WARN__} = sub { warn @_; ++$opt{_error} };
  246.  
  247. my %MPLAYER;    # will store video player arguments
  248.  
  249. my $base_options = <<'BASE';
  250. # Base
  251. [keywords]        : search for YouTube videos
  252. [youtube-url]     : play a video by YouTube URL
  253. :v(ideoid)=ID     : play videos by YouTube video IDs
  254. [playlist-url]    : display videos from a playlistURL
  255. :playlist=ID      : display videos from a playlistID
  256. BASE
  257.  
  258. my $action_options = <<'ACTIONS';
  259. # Actions
  260. :login            : will prompt you for login
  261. :logout           : will delete the authentication key
  262. ACTIONS
  263.  
  264. my $control_options = <<'CONTROL';
  265. # Control
  266. :n(ext)           : get the next page of results
  267. :b(ack)           : get the previous page of results
  268. CONTROL
  269.  
  270. my $other_options = <<'OTHER';
  271. # Others
  272. :r(eturn)         : return to previous section
  273. :refresh          : refresh the current list of results
  274. :dv=i             : display the data structure of result i
  275. -argv -argv2=v    : apply some arguments (e.g.: -u=google)
  276. :reset, :reload   : restart the application
  277. :q, :quit, :exit  : close the application
  278. OTHER
  279.  
  280. my $notes_options = <<'NOTES';
  281. NOTES:
  282.  1. You can specify more options in a row, separated by spaces.
  283.  2. A stdin option is valid only if it begins with '=', ';' or ':'.
  284.  3. Quoting a group of space separated keywords or option-values,
  285.     the group will be considered a single keyword or a single value.
  286. NOTES
  287.  
  288. my $general_help = <<"HELP";
  289.  
  290. $action_options
  291. $control_options
  292. $other_options
  293. $notes_options
  294. Examples:
  295.      3                 : select the 3rd result
  296.     -V funny cats      : search for videos
  297.     -p classical music : search for playlists of videos
  298. HELP
  299.  
  300. my $playlists_help = <<"PL_HELP" . $general_help;
  301.  
  302. # Playlists
  303. :pp=i,i           : play videos from the selected playlists
  304. PL_HELP
  305.  
  306. my $comments_help = <<"COM_HELP" . $general_help;
  307.  
  308. # Comments
  309. :c(omment)        : send a comment to this video
  310. COM_HELP
  311.  
  312. my $complete_help = <<"STDIN_HELP";
  313.  
  314. $base_options
  315. $control_options
  316. $action_options
  317. # YouTube
  318. :i(nfo)=i,i       : display more information
  319. :d(ownload)=i,i   : download the selected videos
  320. :c(omments)=i     : display video comments
  321. :r(elated)=i      : display related videos
  322. :a(uthor)=i       : display author's latest uploads
  323. :p(laylists)=i    : display author's playlists
  324. :ps=i, :s2p=i,i   : save videos to a post-selected playlist
  325. :subscribe=i      : subscribe to author's channel
  326. :(dis)like=i      : like or dislike a video
  327. :fav(orite)=i     : favorite a video
  328.  
  329. # Playing
  330. <number>          : play the corresponding video
  331. 3-8, 3..8         : same as 3 4 5 6 7 8
  332. 8-3, 8..3         : same as 8 7 6 5 4 3
  333. 8 2 12 4 6 5 1    : play the videos in a specific order
  334. 10..              : play all the videos onwards from 10
  335. :q(ueue)=i,i,...  : enqueue videos for playing them later
  336. :pq, :play-queue  : play the enqueued videos (if any)
  337. :anp, :nnp        : auto-next-page, no-next-page
  338. :play=i,i,...     : play a group of selected videos
  339. :regex=my?[regex] : play videos matched by a regex (/i)
  340. :kregex=KEY,RE    : play videos if the value of KEY matches the RE
  341.  
  342. $other_options
  343. $notes_options
  344. ** Examples:
  345. :regex="\\w \\d" -> play videos matched by a regular expression.
  346. :info=1        -> show extra information for the first video.
  347. :d18-20,1,2    -> download the selected videos: 18, 19, 20, 1 and 2.
  348. 3 4 :next 9    -> play the 3rd and 4th videos from the current
  349.                  page, go to the next page and play the 9th video.
  350. STDIN_HELP
  351.  
  352. {
  353.    my $config_documentation = <<"EOD";
  354. #!/usr/bin/perl
  355.  
  356. # $appname $version - configuration file
  357.  
  358. EOD
  359.  
  360.    sub dump_configuration {
  361.        require Data::Dump;
  362.        open my $config_fh, '>', $config_file
  363.          or do { warn "[!] Can't open '${config_file}' for write: $!"; return };
  364.        my $dumped_config = q{our $CONFIG = } . Data::Dump::pp(\%CONFIG) . "\n";
  365.        print $config_fh $config_documentation, $dumped_config;
  366.        close $config_fh;
  367.    }
  368. }
  369.  
  370. if (not -e $config_file or -z _ or $opt{reconfigure}) {
  371.    dump_configuration();
  372. }
  373.  
  374. our $CONFIG;
  375. require $config_file;    # Load the configuration file
  376.  
  377. if (ref $CONFIG ne 'HASH') {
  378.    die "[ERROR] Invalid configuration file!\n\t\$CONFIG is not an HASH ref!";
  379. }
  380.  
  381. # Add audio support to players (backwards compatibility)
  382. while (my ($player, $data) = each %{$CONFIG->{video_players}}) {
  383.    if (    exists $CONFIG{video_players}{$player}
  384.        and not exists $data->{audio}
  385.        and exists $CONFIG{video_players}{$player}{audio}) {
  386.        $data->{audio} = $CONFIG{video_players}{$player}{audio};
  387.    }
  388. }
  389.  
  390. # Get valid config keys
  391. my @valid_keys = grep { exists $CONFIG{$_} } keys %{$CONFIG};
  392. @CONFIG{@valid_keys} = @{$CONFIG}{@valid_keys};
  393.  
  394. {
  395.    my $update_config = 0;
  396.  
  397.    # Define the cache directory
  398.    if (not defined $CONFIG{cache_dir}) {
  399.  
  400.        my $cache_dir =
  401.          ($ENV{XDG_CACHE_HOME} and -d -w $ENV{XDG_CACHE_HOME})
  402.          ? $ENV{XDG_CACHE_HOME}
  403.          : catdir($home_dir, '.cache');
  404.  
  405.        if (not -d -w $cache_dir) {
  406.            $cache_dir = catdir(curdir(), '.cache');
  407.        }
  408.  
  409.        $CONFIG{cache_dir} = catdir($cache_dir, 'youtube-viewer');
  410.        $update_config = 1;
  411.    }
  412.  
  413.    # Locating a video player
  414.    if (not defined $CONFIG{video_player_selected}) {
  415.        foreach my $key (sort keys %{$CONFIG{video_players}}) {
  416.            if (defined(my $abs_player_path = which_command($CONFIG{video_players}{$key}{cmd}))) {
  417.                $CONFIG{video_players}{$key}{cmd} = $abs_player_path;
  418.                $CONFIG{video_player_selected}    = $key;
  419.                $update_config                    = 1;
  420.                last;
  421.            }
  422.        }
  423.    }
  424.  
  425.    # Download with wget if it is installed
  426.    if (not defined $CONFIG{download_with_wget}) {
  427.        if (-x '/usr/bin/wget') {
  428.            $CONFIG{wget_cmd}           = '/usr/bin/wget';
  429.            $CONFIG{download_with_wget} = 1;
  430.        }
  431.        elsif (-x '/usr/local/bin/wget') {
  432.            $CONFIG{wget_cmd}           = '/usr/local/bin/wget';
  433.            $CONFIG{download_with_wget} = 1;
  434.        }
  435.        else {
  436.            $CONFIG{download_with_wget} = 0;
  437.        }
  438.        $update_config = 1;
  439.    }
  440.  
  441.    # Merge into MKV if ffmpeg is installed
  442.    if (not defined $CONFIG{merge_into_mkv}) {
  443.        if (-x '/usr/bin/ffmpeg') {
  444.            $CONFIG{ffmpeg_cmd}     = '/usr/bin/ffmpeg';
  445.            $CONFIG{merge_into_mkv} = 1;
  446.        }
  447.        elsif (-x '/usr/local/bin/ffmpeg') {
  448.            $CONFIG{ffmpeg_cmd}     = '/usr/local/bin/ffmpeg';
  449.            $CONFIG{merge_into_mkv} = 1;
  450.        }
  451.        else {
  452.            $CONFIG{merge_into_mkv} = 0;
  453.        }
  454.        $update_config = 1;
  455.    }
  456.  
  457.    foreach my $key (keys %CONFIG) {
  458.        if (not exists $CONFIG->{$key}) {
  459.            $update_config = 1;
  460.            last;
  461.        }
  462.    }
  463.  
  464.    dump_configuration() if $update_config;
  465. }
  466.  
  467. # Create the cache directory (if needed)
  468. if (not -d $CONFIG{cache_dir}) {
  469.    require File::Path;
  470.    File::Path::make_path($CONFIG{cache_dir})
  471.      or warn "[!] Can't create dir `$CONFIG{cache_dir}': $!";
  472. }
  473.  
  474. @opt{keys %CONFIG} = values(%CONFIG);
  475.  
  476. if ($opt{history}) {
  477.  
  478.    # Create the history file.
  479.    if (not -e $opt{history_file}) {
  480.        open my $fh, '>', $opt{history_file}
  481.          or warn "[!] Can't create the history file `$opt{history_file}': $!";
  482.    }
  483.  
  484.    # Add history to Term::ReadLine
  485.    $term->ReadHistory($opt{history_file});
  486.  
  487.    # All history entries
  488.    my @entries = $term->history_list;
  489.  
  490.    # Rewrite the history file, when the history_limit has been reached.
  491.    if ($opt{history_limit} > 0 and @entries > $opt{history_limit}) {
  492.  
  493.        # Try to create a backup, first
  494.        require File::Copy;
  495.        File::Copy::cp($opt{history_file}, "$opt{history_file}.bak");
  496.  
  497.        if (open my $fh, '>', $opt{history_file}) {
  498.            say {$fh} join("\n", @entries[(@entries - $opt{history_limit} + rand($opt{history_limit} >> 1)) .. $#entries]);
  499.            close $fh;
  500.        }
  501.    }
  502. }
  503.  
  504. {
  505.    my $i = length $key;
  506.    $key =~ s/(.{$i})(.)/$2$1/g while --$i;
  507. }
  508.  
  509. my $yv_obj = WWW::YoutubeViewer->new(
  510.                                     escape_utf8         => 1,
  511.                                     key                 => $key,
  512.                                     config_dir          => $config_dir,
  513.                                     cache_dir           => $opt{cache_dir},
  514.                                     lwp_env_proxy       => $opt{env_proxy},
  515.                                     authentication_file => $authentication_file,
  516.                                    );
  517.  
  518. {
  519.    $yv_obj->set_client_id('923751928481.apps.googleusercontent.com');
  520.    $yv_obj->set_client_secret("\26/Ae]3\b\6\x186a:*#0\32\t\f\n\27\17GC`" ^ substr($key, -24));
  521.     $yv_obj->set_redirect_uri('urn:ietf:wg:oauth:2.0:oob');
  522. }
  523.  
  524. require WWW::YoutubeViewer::Utils;
  525. my $yv_utils = WWW::YoutubeViewer::Utils->new(youtube_url_format => $opt{youtube_video_url},
  526.                                               thousand_separator => $opt{thousand_separator},);
  527.  
  528. {    # Apply the configuration file
  529.     my %temp = %CONFIG;
  530.     apply_configuration(\%temp);
  531. }
  532.  
  533. #---------------------- YOUTUBE-VIEWER USAGE ----------------------#
  534. sub help {
  535.     my $eqs = q{=} x 30;
  536.  
  537.     local $" = ', ';
  538.    print <<"HELP";
  539. \n  $eqs \U$appname\E $eqs
  540.  
  541. usage: $execname [options] ([url] | [keywords])
  542.  
  543. == Base ==
  544.   [URL]             : play an YouTube video by URL
  545.   [keywords]        : search for YouTube videos
  546.   [playlist URL]    : display a playlist of YouTube videos
  547.  
  548.  
  549. == YouTube Options ==
  550.  
  551. * Categories
  552.   -c  --categories     : display the available YouTube categories
  553.   -hl --catlang=s      : language for categories (default: en_US)
  554.  
  555. * Region
  556.   --region=s           : set the region code (default: US)
  557.  
  558. * Videos
  559.   -uv --user-vid=s     : list videos uploaded by a specific user
  560.   -cv --channel-vid=s  : list videos uploaded to a specific channel
  561.   -uf --user-fav=s     : list the videos favorited by a specific user
  562.   -id --videoids=s,s   : play YouTube videos by their IDs
  563.   -rv --related=s      : show related videos for a video ID or URL
  564.       --search=s       : search for YouTube videos (default mode)
  565.  
  566. * Playlists
  567.   -p  --playlists    : search for playlists of videos
  568.       --pid=s        : list a playlist of videos by playlistID
  569.       --pp=s,s       : play the videos from the given playlist IDs
  570.       --ps=s         : add videos by ID or URL to a post-selected playlist
  571.                        or in a given playlistID specified with `--pid`
  572.       --position=i   : the position in a playlist where to add a video
  573.   -up --user-pl=s    : list the playlists created by a specific user
  574.   -cp --channel-pl=s : list the playlists belonging to a specific channel ID
  575.  
  576. * Channels
  577.   --channels         : search for Youtube channels
  578.  
  579. * Comments
  580.   --comments=s         : display comments for a YouTube video by ID or URL
  581.  
  582. * Filtering
  583.   --author=s           : search in videos uploaded by a specific user
  584.   --channel-id=s       : search in videos belonging to a specific channel ID
  585.   --duration=s         : filter search results based on video length
  586.                          valid values are: short, medium, long
  587.   --caption=s          : only videos with/without closed captions
  588.                          valid values are: any, closedCaption, none
  589.   --category=i         : search only for videos in a specific category ID
  590.   --safe-search=s      : YouTube will skip restricted videos for your location
  591.                          valid values are: none, moderate, strict
  592.   --order=s            : order the results using a specific sorting method
  593.                          valid values: date rating viewCount title videoCount
  594.   --within=s           : show only videos uploaded within the specified time
  595.                          valid values are: Nd, Nm, Ny, where N is a number
  596.   --hd!                : search only for videos available in at least 720p
  597.   --vd=s               : set the video definition (any, high or standard)
  598.   --page=i             : get results starting with a specific page
  599.   --results=i          : how many results to display per page (max: 50)
  600.   -2  -3  -4  -7  -1   : resolutions: 240p, 360p, 480p, 720p and 1080p
  601.   --resolution=s       : supported resolutions: original, 2160p, 1440p,
  602.                          1080p, 720p, 480p, 360p, 240p, 144p, audio.
  603.  
  604. * Account
  605.   --login              : will prompt for authentication (OAuth 2.0)
  606.   --logout             : will delete the authentication key
  607.  
  608. * [GET] Personal
  609.   -F  --favorites:s     : show the latest favorited videos *
  610.   -S  --subscriptions:s : show the subscribed channels *
  611.   -SV --subs-videos:s   : show the subscription videos *
  612.       --subs-order=s    : change the subscription order
  613.                           valid values: alphabetical, relevance, unread
  614.   -L  --likes           : show the videos that you liked on YouTube *
  615.       --dislikes        : show the videos that you disliked on YouTube *
  616.  
  617. * [POST] Personal
  618.   --subscribe=s        : subscribe to a channel *
  619.   --user-subscribe=s   : subscribe to a channel via username *
  620.   --favorite=s         : favorite a YouTube video by URL or ID *
  621.   --like=s             : send a 'like' rating to a video URL or ID *
  622.   --dislike=s          : send a 'dislike' rating to a video URL or ID *
  623.  
  624.  
  625. == Player Options ==
  626.  
  627. * Arguments
  628.   -f  --fullscreen!  : set the fullscreen mode for the selected video player
  629.   -n  --novideo!     : play the music only without a video in the foreground
  630.   --vo=s             : specify the video output for MPlayer
  631.   --af=s             : specify an audio filter for MPlayer
  632.   --append-arg=s     : append some command-line parameters to the media player
  633.   --player=s         : select a player to stream videos
  634.                        available players: @{[keys %{$CONFIG->{video_players}}]}
  635.  
  636.  
  637. == Download Options ==
  638.  
  639. * Download
  640.   -d  --download!       : activate the download mode
  641.   -dp --dl-play!        : play the video after download (with -d)
  642.   -rp --rem-played!     : delete a local video after played (with -dp)
  643.       --wget-dl!        : download videos with wget (recommended)
  644.       --dl-parallel     : download multiple videos at once
  645.       --clobber!        : overwrite an existent video (with -d)
  646.       --skip-if-exists! : don't download videos which already exists (with -d)
  647.       --copy-caption!   : copy and rename the caption for downloaded videos
  648.       --downloads-dir=s : downloads directory (set: '$opt{downloads_dir}')
  649.       --filename=s      : set a custom format for the video filename (see: -T)
  650.       --fat32safe!      : makes filenames FAT32 safe (includes Unicode)
  651.       --mkv-merge!      : merge audio and video into an MKV container
  652.       --merge-captions! : include closed-captions into the MKV container
  653.  
  654. * Convert
  655.   --convert-cmd=s      : command for converting videos after download
  656.                          which include the *IN* and *OUT* tokens
  657.   --convert-to=s       : convert video to a specific format (with -d)
  658.   --keep-original!     : keep the original video after converting
  659.  
  660.  
  661. == Other Options ==
  662.  
  663. * Behavior
  664.   -A  --all!            : play all the video results in order
  665.   -B  --backwards!      : play all video results in reverse order
  666.   -s  --shuffle!        : shuffle the results of videos and playlists
  667.   -I  --interactive!    : interactive mode, prompting for user input
  668.       --std-input=s     : use this value as the first standard input
  669.       --max-seconds=i   : ignore videos longer than i seconds
  670.       --min-seconds=i   : ignore videos shorter than i seconds
  671.       --combine-multi!  : combine multiple videos into one play instance
  672.       --get-term-width! : allow $execname to read your terminal width
  673.       --autohide!       : automatically hide watched videos
  674.       --highlight!      : remember and highlight selected videos
  675.       --confirm!        : show a confirmation message after each play
  676.       --prefer-mp4!     : prefer videos in MP4 format, instead of WEBM
  677.  
  678. * Closed-captions
  679.   --get-captions!      : download the closed captions for videos
  680.   --auto-captions!     : include or exclude auto-generated captions
  681.   --captions-dir=s     : the directory where to download the .srt files
  682.  
  683. * Config
  684.   -U  --update-config! : update the configuration file before exit
  685.  
  686. * Output
  687.   -C  --colorful!      : use colors to delimit the video results
  688.   -D  --details!       : a new look for the results, with more details
  689.   -W  --fixed-width!   : adjust the results to fit inside the term width
  690.       --custom-layout! : display the results using a custom layout (see conf)
  691.   -i  --info=s         : show some info for a videoID or URL
  692.   -e  --extract=s      : extract information from videos (see: -T)
  693.       --extract-file=s : extract the information from videos in this file
  694.       --dump=format    : dump metadata information in `videoID.format` files
  695.                          valid formats: json, perl
  696.   -q  --quiet          : do not display any warning
  697.       --really-quiet   : do not display any warning or output
  698.       --escape-info!   : quotemeta() the fields of the `--extract`
  699.       --use-colors!    : enable or disable the ANSI colors for text
  700.  
  701. * Other
  702.   --proxy=s            : set HTTP(S)/SOCKS proxy: 'proto://domain.tld:port/'
  703.                          If authentication required,
  704.                          use 'proto://user:pass\@domain.tld:port/'
  705.   --dash!              : include or exclude the DASH itags
  706.   --dash-mp4a!         : include or exclude the itags for MP4 audio streams
  707.  
  708.  
  709. Help options:
  710.   -T  --tricks         : show more 'hidden' features of $execname
  711.   -E  --examples       : show some useful usage examples for $execname
  712.   -H  --stdin-help     : show the valid stdin options for $execname
  713.   -v  --version        : print version and exit
  714.   -h  --help           : print help and exit
  715.       --debug:[1,2]    : see behind the scenes
  716.  
  717. NOTES:
  718.    *    -> requires authentication
  719.    !    -> the argument can be negated with '--no'
  720.    =i   -> requires an integer argument
  721.    =s   -> requires an argument
  722.    :s   -> can take an optional argument
  723.    =s,s -> can take more arguments separated by commas
  724.  
  725. HELP
  726.    main_quit(0);
  727. }
  728.  
  729. sub wrap_text {
  730.    my (%args) = @_;
  731.  
  732.    require Text::Wrap;
  733.    local $Text::Wrap::columns = ($args{columns} || $term_width) - 8;
  734.  
  735.    my $text = "@{$args{text}}";
  736.    $text =~ tr{\r}{}d;
  737.  
  738.    return eval { Text::Wrap::wrap($args{i_tab}, $args{s_tab}, $text) } // $text;
  739. }
  740.  
  741. sub tricks {
  742.    print <<"TRICKS";
  743.  
  744.                == youtube-viewer -- tips and tricks ==
  745.  
  746. -> Playing videos
  747. > To stream the videos in other players, you need to change the
  748.   configuration file. Where it says "video_player_selected", change it
  749.   to any player which is defined inside the "video_players" hash.
  750.  
  751. -> Arguments
  752. > Almost all boolean arguments can be negated with a "--no-" prefix.
  753. > Arguments that require an ID/URL, you can specify more than one,
  754.   separated by whitespace (quoted), or separated by commas.
  755.  
  756. -> My channel
  757. > Starting with version 3.2.1, it's possible to use the string "mine"
  758.   in place where a channel ID is required. Doing this, "mine" will be
  759.   replaced with your channel ID. (requires authentication)
  760.  
  761.   Examples:
  762.        $execname --channel-playlists=mine
  763.        $execname --channel-videos=mine
  764.        $execname --likes=mine
  765.        $execname --favorites=mine
  766.  
  767. -> More STDIN help:
  768. > ":r", ":return" will return to the previous section.
  769.   For example, if you search for playlists, then select a playlist
  770.   of videos, inserting ":r" will return back to the playlist results.
  771.   Also, for the previous page, you can insert ':b', but ':r' is faster!
  772.  
  773. > "6" (quoted) or -V=6 will search for videos with the keyword '6'.
  774.  
  775. > If a stdin option is followed by one or more digits, the equal sign,
  776.   which separates the option from value, can be omitted.
  777.   For example:
  778.        :i2,4  is equivalent with :i=2,4
  779.        :d1-5  is equivalent with :d=1,2,3,4,5
  780.        :c10   is equivalent with :c=10
  781.  
  782. > When more videos are selected to play, you can stop them by
  783.   pressing CTRL+C. $execname will return to the previous section.
  784.  
  785. > Space inside the values of STDIN options, can be either quoted
  786.   or backslashed.
  787.   For example:
  788.        :re=video\\ title     ==     :re="video title"
  789.  
  790. > ":anp" stands for the "Auto Next Page". How do we use it?
  791.   Well, let's search for some videos. Now, if we'd want to play
  792.   only the videos matched by a regex, we'd say :re="REGEX".
  793.   But, what if we'd want to play the videos from the next pages too?
  794.   In this case, ":anp" is your friend. Use it wisely!
  795.  
  796. -> Special tokens:
  797.  
  798.  *ID*          : the YouTube video ID
  799.  *AUTHOR*      : the author name of the video
  800.  *CHANNELID*   : the channel ID of the video
  801.  *RESOLUTION*  : the resolution of the video
  802.  *VIEWS*       : the number of views
  803.  *LIKES*       : the number of likes
  804.  *DISLIKES*    : the number of dislikes
  805.  *RATING*      : the rating of the video from 0 to 5
  806.  *COMMENTS*    : the number of comments
  807.  *DURATION*    : the duration of the video in seconds
  808.  *DIMENSION*   : the dimension of the video (2D or 3D)
  809.  *DEFINITION*  : the definition of the video (HD or SD)
  810.  *TIME*        : the duration of the video in HH::MM::SS
  811.  *TITLE*       : the title of the video
  812.  *FTITLE*      : the title of the video (filename safe)
  813.  *DESCRIPTION* : the description of the video
  814.  
  815.  *URL*      : the YouTube URL of the video
  816.  *ITAG*     : the itag value of the video
  817.  *FORMAT*   : the extension of the video (without the dot)
  818.  
  819.  *CAPTION*  : true if the video has caption.
  820.  *SUB*      : the local subtitle file (if any)
  821.  *AUDIO*    : the audio URL of the video (only in DASH mode)
  822.  *VIDEO*    : the video URL of the video (it might not contain audio)
  823.  *AOV*      : audio URL (if any) or video URL (in this order)
  824.  
  825. -> Special escapes:
  826.    \\t                  tab
  827.    \\n                  newline
  828.    \\r                  return
  829.    \\f                  form feed
  830.    \\b                  backspace
  831.    \\a                  alarm (bell)
  832.    \\e                  escape
  833.  
  834. -> Extracting information from videos:
  835. > Extracting information can be achieved by using the "--extract" command-line
  836.   option which takes a given format as its argument, which is defined by using
  837.   special tokens, special escapes or literals.
  838.  
  839.   Example:
  840.     $execname --no-interactive --extract '*TITLE* (*ID*)' [URL]
  841.  
  842. -> Configuration file: $config_file
  843.  
  844. -> Donations gladly accepted:
  845.    https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=75FUVBE6Q73T8
  846.  
  847. TRICKS
  848.    main_quit(0);
  849. }
  850.  
  851. sub examples {
  852.    print <<"EXAMPLES";
  853. ==== COMMAND LINE EXAMPLES ====
  854.  
  855. Command: $execname -A -n -4 russian music -category=10
  856. Results: play all the video results (-A)
  857.         only audio, no video (-n)
  858.         quality 480p (-4)
  859.         search for "russian music"
  860.         in the "10" category, which is the Music category.
  861.         -A will include the videos from the next pages as well.
  862.  
  863. Command: $execname --comments 'https://www.youtube.com/watch?v=U6_8oIPFREY'
  864. Results: show video comments for a specific video URL or videoID
  865.  
  866. Command: $execname --results=5 -up=khanacademy -D
  867. Results: set 5 results,
  868.         get playlists created by a specific user
  869.         and print them with details (-D)
  870.  
  871. Command: $execname --author=UCBerkeley atom
  872. Results: search only in videos uploaded by a specific author
  873.  
  874. Command: $execname -S=vsauce
  875. Results: get the video subscriptions for a username
  876.  
  877. Command: $execname --page=2 -u=Google
  878. Results: show latest videos uploaded by Google,
  879.         starting with the page number 2.
  880.  
  881. Command: $execname cats --order=viewCount --duration=short
  882. Results: search for 'cats' videos, ordered by ViewCount and short duration.
  883.  
  884. Command: $execname --channels russian music
  885. Results: search for channels.
  886.  
  887. Command: $execname -uf=Google
  888. Results: show latest videos favorited by a user.
  889.  
  890.  
  891. ==== USER INPUT EXAMPLES ====
  892.  
  893. A STDIN option can begin with ':', ';' or '='.
  894.  
  895. Command: <ENTER>, :n, :next
  896. Results: get the next page of results.
  897.  
  898. Command: :b, :back (:r, :return)
  899. Results: get the previous page of results.
  900.  
  901. Command: :i4..6, :i7-9, :i20-4, :i2, :i=4, :info=4
  902. Results: show extra information for the selected videos.
  903.  
  904. Command: :d5,2, :d=3, :download=8
  905. Results: download the selected videos.
  906.  
  907. Command: :c2, :comments=4
  908. Results: show comments for a selected video.
  909.  
  910. Command: :r4, :related=6
  911. Results: show related videos for a selected video.
  912.  
  913. Command: :a14, :author=12
  914. Results: show videos uploaded by the author who uploaded the selected video.
  915.  
  916. Command: :p9, :playlists=14
  917. Results: show playlists created by the author who uploaded the selected video.
  918.  
  919. Command: :subscribe=7
  920. Results: subscribe to the author's channel who uploaded the selected video.
  921.  
  922. Command: :like=2, :dislike=4,5
  923. Results: like or dislike the selected videos.
  924.  
  925. Command: :fav=4, :favorite=3..5
  926. Results: favorite the selected videos.
  927.  
  928. Command: 3, 5..7, 12-1, 9..4, 2 3 9
  929. Results: play the selected videos.
  930.  
  931. Command: :q3,5, :q=4, :queue=3-9
  932. Results: enqueue the selected videos to play them later.
  933.  
  934. Command: :pq, :play-queue
  935. Results: play the videos enqueued by the :queue option.
  936.  
  937. Command: :re="^Linux"
  938. Results: play videos matched by a regex.
  939. Example: valid title: "Linux video"
  940.  
  941. Command: :regex="linux.*part \\d+/\\d+"
  942. Example: valid title: "Introduction to Linux (part 1/4)"
  943.  
  944. Command: :anp 1 2 3
  945. Results: play the first three videos from every page.
  946.  
  947. Command: :r, :return
  948. Results: return to the previous section.
  949. EXAMPLES
  950.    main_quit(0);
  951. }
  952.  
  953. sub stdin_help {
  954.    print $complete_help;
  955.    main_quit(0);
  956. }
  957.  
  958. # Print version
  959. sub version {
  960.    print "YouTube Viewer $version\n";
  961.    main_quit(0);
  962. }
  963.  
  964. sub apply_configuration {
  965.    my ($opt, $keywords) = @_;
  966.  
  967.    if ($yv_obj->get_debug == 2
  968.        or (defined($opt->{debug}) && $opt->{debug} == 2)) {
  969.        require Data::Dump;
  970.        say "=>> Options with keywords: <@{$keywords}>";
  971.        Data::Dump::pp($opt);
  972.    }
  973.  
  974.    # ... BASIC OPTIONS ... #
  975.    if (delete $opt->{quiet}) {
  976.        close STDERR;
  977.    }
  978.  
  979.    if (delete $opt->{really_quiet}) {
  980.        close STDERR;
  981.        close STDOUT;
  982.    }
  983.  
  984.    # ... YOUTUBE OPTIONS ... #
  985.    foreach my $option_name (
  986.                             qw(
  987.                             videoCaption maxResults order
  988.                             videoDefinition videoCategoryId
  989.                             videoDimension videoDuration
  990.                             videoEmbeddable videoLicense
  991.                             videoSyndicated channelId
  992.                             publishedAfter publishedBefore
  993.                             safeSearch regionCode debug hl
  994.                             http_proxy page subscriptions_order
  995.                             )
  996.      ) {
  997.  
  998.        if (defined $opt->{$option_name}) {
  999.            my $code      = \&{"WWW::YoutubeViewer::set_$option_name"};
  1000.            my $value     = delete $opt->{$option_name};
  1001.            my $set_value = $yv_obj->$code($value);
  1002.  
  1003.            if (not defined($set_value) or $set_value ne $value) {
  1004.                warn "\n[!] Invalid value <$value> for option <$option_name>\n";
  1005.            }
  1006.        }
  1007.    }
  1008.  
  1009.    if (defined $opt->{prefer_mp4}) {
  1010.        $yv_obj->set_prefer_mp4(delete($opt->{prefer_mp4}) ? 1 : 0);
  1011.    }
  1012.  
  1013.    if (defined $opt->{hd}) {
  1014.        $yv_obj->set_videoDefinition(delete($opt->{hd}) ? 'high' : 'any');
  1015.    }
  1016.  
  1017.    if (defined $opt->{author}) {
  1018.        my $username = delete $opt->{author};
  1019.        $yv_obj->set_channelId($yv_obj->channel_id_from_username($username) // $username);
  1020.    }
  1021.  
  1022.    if (defined $opt->{within}) {
  1023.        my $value = delete $opt->{within};
  1024.  
  1025.        if ($value =~ /^\s*(\d+(?:\.\d+)?)([dmy])/i) {
  1026.            my $date = $yv_utils->period_to_date($1, $2);
  1027.            $yv_obj->set_publishedAfter($date);
  1028.        }
  1029.        else {
  1030.            warn "\n[!] Invalid value <$value> for option `--within`!\n";
  1031.        }
  1032.    }
  1033.  
  1034.    if (defined $opt->{more_results}) {
  1035.        $yv_obj->set_maxResults(delete($opt->{more_results}) ? 50 : $CONFIG{maxResults});
  1036.    }
  1037.  
  1038.    if (delete $opt->{authenticate}) {
  1039.        authenticate();
  1040.    }
  1041.  
  1042.    if (delete $opt->{logout}) {
  1043.        logout();
  1044.    }
  1045.  
  1046.    # ... OTHER OPTIONS ... #
  1047.    if (defined $opt->{extract_info_file}) {
  1048.        open my $fh, '>:utf8', delete($opt->{extract_info_file});
  1049.        $opt{extract_info_fh} = $fh;
  1050.    }
  1051.  
  1052.    if (defined $opt->{colors}) {
  1053.        $opt{_colors} = $opt->{colors};
  1054.        if (delete $opt->{colors}) {
  1055.            require Term::ANSIColor;
  1056.            *colored    = \&Term::ANSIColor::colored;
  1057.            *colorstrip = \&Term::ANSIColor::colorstrip;
  1058.        }
  1059.        else {
  1060.            *colored    = sub { $_[0] };
  1061.            *colorstrip = sub { $_[0] };
  1062.        }
  1063.    }
  1064.  
  1065.    # ... SUBROUTINE CALLS ... #
  1066.    if (defined $opt->{subscribe_channel}) {
  1067.        subscribe_to_channels(split(/[,\s]+/, delete $opt->{subscribe_channel}));
  1068.    }
  1069.  
  1070.    if (defined $opt->{subscribe_username}) {
  1071.        subscribe_to_usernames(split(/[,\s]+/, delete $opt->{subscribe_username}));
  1072.    }
  1073.  
  1074.    if (defined $opt->{favorite_video}) {
  1075.        favorite_videos(split(/[,\s]+/, delete $opt->{favorite_video}));
  1076.    }
  1077.  
  1078.    if (defined $opt->{playlist_save}) {
  1079.        my @ids = split(/[,\s]+/, delete $opt->{playlist_save});
  1080.        if (defined $opt->{playlist_id}) {
  1081.            save_to_playlist(get_valid_playlist_id(delete $opt->{playlist_id}) // (return), @ids);
  1082.        }
  1083.        else {
  1084.            select_and_save_to_playlist(@ids);
  1085.        }
  1086.    }
  1087.  
  1088.    if (defined $opt->{like_video}) {
  1089.        rate_videos('like', split(/[,\s]+/, delete $opt->{like_video}));
  1090.    }
  1091.  
  1092.    if (defined $opt->{dislike_video}) {
  1093.        rate_videos('dislike', split(/[,\s]+/, delete $opt->{dislike_video}));
  1094.    }
  1095.  
  1096.    if (defined $opt->{play_video_ids}) {
  1097.        get_and_play_video_ids(split(/[,\s]+/, delete $opt->{play_video_ids}));
  1098.    }
  1099.  
  1100.    if (defined $opt->{play_playlists}) {
  1101.        get_and_play_playlists(split(/[,\s]+/, delete $opt->{play_playlists}));
  1102.    }
  1103.  
  1104.    if (defined $opt->{playlist_id}) {
  1105.        my $playlistID = get_valid_playlist_id(delete($opt->{playlist_id})) // return;
  1106.        get_and_print_videos_from_playlist($playlistID);
  1107.    }
  1108.  
  1109.    if (defined $opt->{search_playlists}) {
  1110.        my $value = delete($opt->{search_playlists});
  1111.        if ($value =~ /$valid_playlist_id_re/ and not @{$keywords}) {
  1112.            get_and_print_videos_from_playlist($value);
  1113.        }
  1114.        else {
  1115.            print_playlists($yv_obj->search_playlists([$value, @{$keywords}]));
  1116.        }
  1117.    }
  1118.  
  1119.    if (defined $opt->{search_videos}) {
  1120.        my $value = delete $opt->{search_videos};
  1121.        print_videos($yv_obj->search_videos([$value, @{$keywords}]));
  1122.    }
  1123.  
  1124.    if (defined $opt->{search_channels}) {
  1125.        my $value = delete $opt->{search_channels};
  1126.        print_channels($yv_obj->search_channels([$value, @{$keywords}]));
  1127.    }
  1128.  
  1129.    if (delete $opt->{categories}) {
  1130.        print_categories($yv_obj->video_categories);
  1131.    }
  1132.  
  1133.    if (defined $opt->{user_videos}) {
  1134.        my $id = delete $opt->{user_videos};
  1135.        if ($id =~ /$valid_channel_id_re/) {
  1136.            print_videos($yv_obj->uploads_from_username($+{channel_id}));
  1137.        }
  1138.        else {
  1139.            warn_invalid("username", $id);
  1140.        }
  1141.    }
  1142.  
  1143.    if (defined $opt->{channel_id_videos}) {
  1144.        my $id = delete $opt->{channel_id_videos};
  1145.        if ($id =~ /$valid_channel_id_re/) {
  1146.            print_videos($yv_obj->uploads($+{channel_id}));
  1147.        }
  1148.        else {
  1149.            warn_invalid("channelID", $id);
  1150.        }
  1151.    }
  1152.  
  1153.    if (defined $opt->{subscriptions}) {
  1154.        my $username = delete $opt->{subscriptions};
  1155.        print_channels($username ? $yv_obj->subscriptions_from_username($username) : $yv_obj->subscriptions);
  1156.    }
  1157.  
  1158.    if (defined $opt->{subscription_videos}) {
  1159.        my $username = delete $opt->{subscription_videos};
  1160.        print_videos($username ? $yv_obj->subscription_videos_from_username($username) : $yv_obj->subscription_videos);
  1161.    }
  1162.  
  1163.    if (defined $opt->{related_videos}) {
  1164.        get_and_print_related_videos(split(/[,\s]+/, delete($opt->{related_videos})));
  1165.    }
  1166.  
  1167.    if (defined $opt->{user_playlists}) {
  1168.        my $id = delete $opt->{user_playlists};
  1169.  
  1170.        if ($id =~ /$valid_channel_id_re/) {
  1171.            print_playlists($yv_obj->playlists_from_username($+{channel_id}));
  1172.        }
  1173.        else {
  1174.            warn_invalid("username", $id);
  1175.        }
  1176.    }
  1177.  
  1178.    if (defined $opt->{channel_playlists}) {
  1179.        my $id = delete $opt->{channel_playlists};
  1180.  
  1181.        if ($id =~ /$valid_channel_id_re/) {
  1182.            print_playlists($yv_obj->playlists($+{channel_id}));
  1183.        }
  1184.        else {
  1185.            warn_invalid("channelID", $id);
  1186.        }
  1187.    }
  1188.  
  1189.    if (defined $opt->{favorites}) {
  1190.        my $channel_id = delete($opt->{favorites});
  1191.  
  1192.        print_videos(
  1193.                       $channel_id
  1194.                     ? $yv_obj->favorites($channel_id)
  1195.                     : $yv_obj->favorites
  1196.                    );
  1197.    }
  1198.  
  1199.    if (defined $opt->{likes}) {
  1200.        my $channel_id = delete($opt->{likes});
  1201.  
  1202.        print_videos(
  1203.                       $channel_id
  1204.                     ? $yv_obj->likes($channel_id)
  1205.                     : $yv_obj->my_likes
  1206.                    );
  1207.    }
  1208.  
  1209.    if (defined $opt->{dislikes}) {
  1210.        delete $opt->{dislikes};
  1211.        print_videos($yv_obj->my_dislikes);
  1212.    }
  1213.  
  1214.    if (defined $opt->{user_favorited_videos}) {
  1215.        my $username = delete $opt->{user_favorited_videos};
  1216.  
  1217.        if ($username =~ /$valid_channel_id_re/) {
  1218.            print_videos($yv_obj->favorites_from_username($+{channel_id}));
  1219.        }
  1220.        else {
  1221.            warn_invalid("username", $username);
  1222.        }
  1223.    }
  1224.  
  1225.    if (defined $opt->{user_liked_videos}) {
  1226.        my $username = delete $opt->{user_liked_videos};
  1227.  
  1228.        if ($username =~ /$valid_channel_id_re/) {
  1229.            print_videos($yv_obj->likes_from_username($+{channel_id}));
  1230.        }
  1231.        else {
  1232.            warn_invalid("username", $username);
  1233.        }
  1234.    }
  1235.  
  1236.    if (defined $opt->{get_comments}) {
  1237.        get_and_print_comments(split(/[,\s]+/, delete($opt->{get_comments})));
  1238.    }
  1239.  
  1240.    if (defined $opt->{print_video_info}) {
  1241.        get_and_print_video_info(split(/[,\s]+/, delete $opt->{print_video_info}));
  1242.    }
  1243. }
  1244.  
  1245. sub parse_arguments {
  1246.    my ($keywords) = @_;
  1247.  
  1248.    state $x = do {
  1249.        require Getopt::Long;
  1250.        Getopt::Long::Configure('no_ignore_case');
  1251.    };
  1252.  
  1253.    Getopt::Long::GetOptions(
  1254.  
  1255.        # Main options
  1256.        'help|usage|h|?'        => \&help,
  1257.        'examples|E'            => \&examples,
  1258.        'stdin-help|shelp|sh|H' => \&stdin_help,
  1259.        'tricks|tips|T'         => \&tricks,
  1260.        'version|v'             => \&version,
  1261.        'update-config|U!'      => \&dump_configuration,
  1262.  
  1263.        # Resolutions
  1264.        '240p|2'  => sub { $opt{resolution} = 240 },
  1265.        '360p|3'  => sub { $opt{resolution} = 360 },
  1266.        '480p|4'  => sub { $opt{resolution} = 480 },
  1267.        '720p|7'  => sub { $opt{resolution} = 720 },
  1268.        '1080p|1' => sub { $opt{resolution} = 1080 },
  1269.        'res|resolution=s' => \$opt{resolution},
  1270.  
  1271.        'comments=s'                  => \$opt{get_comments},
  1272.        'search|videos|V:s'           => \$opt{search_videos},
  1273.        'video-ids|videoids|id|ids=s' => \$opt{play_video_ids},
  1274.  
  1275.        'c|categories'               => \$opt{categories},
  1276.        'channels|search-channels:s' => \$opt{search_channels},
  1277.  
  1278.        'subscriptions|S:s'                 => \$opt{subscriptions},
  1279.        'subs-videos|SV:s'                  => \$opt{subscription_videos},
  1280.        'subs-order=s'                      => \$opt{subscriptions_order},
  1281.        'favorites|fv|favorited-videos|F:s' => \$opt{favorites},
  1282.        'likes|L:s'                         => \$opt{likes},
  1283.        'dislikes'                          => \$opt{dislikes},
  1284.        'subscribe=s'                       => \$opt{subscribe_channel},
  1285.        'user-subscribe=s'                  => \$opt{subscribe_username},
  1286.        'cv|channel|channel-videos=s'       => \$opt{channel_id_videos},
  1287.        'cp|channel-playlists=s'            => \$opt{channel_playlists},
  1288.  
  1289.        # English-UK friendly
  1290.        'favorite|favourite|favorite-video|favourite-video|fav=s' => \$opt{favorite_video},
  1291.  
  1292.        'login|authenticate'      => \$opt{authenticate},
  1293.        'logout'                  => \$opt{logout},
  1294.        'user|user-videos|u|uv=s' => \$opt{user_videos},
  1295.        'user-playlists|up=s'     => \$opt{user_playlists},
  1296.        'user-favorites|uf=s'     => \$opt{user_favorited_videos},
  1297.        'user-likes|ul=s'         => \$opt{user_liked_videos},
  1298.        'related-videos|rl|rv=s'  => \$opt{related_videos},
  1299.  
  1300.        'http_proxy|http-proxy|proxy=s' => \$opt{http_proxy},
  1301.  
  1302.        'catlang|cl|hl=s'        => \$opt{hl},
  1303.        'category|cat-id|cat=i'  => \$opt{videoCategoryId},
  1304.        'r|region|region-code=s' => \$opt{regionCode},
  1305.  
  1306.        'orderby|order|order-by=s' => \$opt{order},
  1307.        'duration=s'               => \$opt{videoDuration},
  1308.        'within=s'                 => \$opt{within},
  1309.  
  1310.        'max-seconds|max_seconds=i' => \$opt{max_seconds},
  1311.        'min-seconds|min_seconds=i' => \$opt{min_seconds},
  1312.  
  1313.        'like=s'                     => \$opt{like_video},
  1314.        'dislike=s'                  => \$opt{dislike_video},
  1315.        'author=s'                   => \$opt{author},
  1316.        'channel-id=s'               => \$opt{channelId},
  1317.        'all|A|play-all!'            => \$opt{play_all},
  1318.        'backwards|B!'               => \$opt{play_backwards},
  1319.        'input|std-input=s'          => \$opt{std_input},
  1320.        'use-colors|colors|colored!' => \$opt{colors},
  1321.  
  1322.        'playlists|p|pl|playlist:s' => \$opt{search_playlists},
  1323.        'pid|playlist-id=s'         => \$opt{playlist_id},
  1324.  
  1325.        'play-playlists|pp=s'      => \$opt{play_playlists},
  1326.        'debug:1'                  => \$opt{debug},
  1327.        'download|dl|d!'           => \$opt{download_video},
  1328.        'safe-search|safeSearch=s' => \$opt{safeSearch},
  1329.        'vd|video-definition=s'    => \$opt{videoDefinition},
  1330.        'hd|high-definition!'      => \$opt{hd},
  1331.        'I|interactive!'           => \$opt{interactive},
  1332.        'convert-to|convert_to=s'  => \$opt{convert_to},
  1333.        'keep-original-video!'     => \$opt{keep_original_video},
  1334.        'e|extract|extract-info=s' => \$opt{extract_info},
  1335.        'extract-file=s'           => \$opt{extract_info_file},
  1336.        'escape-info!'             => \$opt{escape_info},
  1337.  
  1338.        'dump=s' => sub {
  1339.            my (undef, $format) = @_;
  1340.            $opt{dump} = (
  1341.                ($format =~ /json/i) ? 'json' : ($format =~ /perl/i) ? 'perl' : do {
  1342.                    warn "[!] Invalid format <<$format>> for option --dump\n";
  1343.                    undef;
  1344.                  }
  1345.            );
  1346.        },
  1347.  
  1348.        # Set a video player
  1349.        'player|vplayer|video-player|video_player=s' => sub {
  1350.  
  1351.            if (not exists $opt{video_players}{$_[1]}) {
  1352.                die "[!] Unknown video player selected: <<$_[1]>>\n";
  1353.            }
  1354.  
  1355.            $opt{video_player_selected} = $_[1];
  1356.        },
  1357.  
  1358.        'append-mplayer|append-arg|arg=s' => \$MPLAYER{user_defined_arguments},
  1359.        'vo=s'                            => sub { $MPLAYER{video_output} = "-vo $_[1]" },
  1360.        'af=s'                            => sub { $MPLAYER{audio_filter} = "-af $_[1]" },
  1361.  
  1362.        # Others
  1363.        'colorful|colourful|C!' => \$opt{results_with_colors},
  1364.        'details|D!'            => \$opt{results_with_details},
  1365.        'fixed-width|W|fw!'     => \$opt{results_fixed_width},
  1366.        'caption=s'             => \$opt{videoCaption},
  1367.        'fullscreen|fs|f!'      => \$opt{fullscreen},
  1368.        'dash!'                 => \$opt{dash_support},
  1369.        'confirm!'              => \$opt{confirm},
  1370.        'prefer-mp4!'           => \$opt{prefer_mp4},
  1371.  
  1372.        'custom-layout!'         => \$opt{custom_layout},
  1373.        'custom-layout-format=s' => \$opt{custom_layout_format},
  1374.  
  1375.        'merge-into-mkv|mkv-merge!'           => \$opt{merge_into_mkv},
  1376.        'merge-with-captions|merge-captions!' => \$opt{merge_with_captions},
  1377.  
  1378.        'convert-command|convert-cmd=s'      => \$opt{convert_cmd},
  1379.        'dash-m4a|dash-mp4-audio|dash-mp4a!' => \$opt{dash_mp4_audio},
  1380.        'wget-dl|wget-download!'             => \$opt{download_with_wget},
  1381.        'dl-parallel|download-in-parallel!'  => \$opt{download_in_parallel},
  1382.        'filename|filename-format=s'         => \$opt{video_filename_format},
  1383.        'rp|rem-played|remove-played-file!'  => \$opt{remove_played_file},
  1384.        'clobber!'                           => \$opt{clobber},
  1385.        'info|i|video-info=s'                => \$opt{print_video_info},
  1386.        'get-term-width!'                    => \$opt{get_term_width},
  1387.        'page=i'                             => \$opt{page},
  1388.        'novideo|no-video|n!'                => \$opt{novideo},
  1389.        'autohide!'                          => \$opt{autohide_watched},
  1390.        'highlight!'                         => \$opt{highlight_watched},
  1391.        'results=i'                          => \$opt{maxResults},
  1392.        'shuffle|s!'                         => \$opt{shuffle},
  1393.        'more|m!'                            => \$opt{more_results},
  1394.        'combine-multiple-videos|combine!'   => \$opt{combine_multiple_videos},
  1395.        'pos|position=i'                     => \$opt{position},
  1396.        'ps|playlist-save=s'                 => \$opt{playlist_save},
  1397.  
  1398.        'quiet|q!'      => \$opt{quiet},
  1399.        'really-quiet!' => \$opt{really_quiet},
  1400.  
  1401.        'dp|downl-play|download-and-play|dl-play!' => \$opt{download_and_play},
  1402.  
  1403.        'thousand-separator=s'           => \$opt{thousand_separator},
  1404.        'get-captions|get_captions!'     => \$opt{get_captions},
  1405.        'auto-captions|auto_captions!'   => \$opt{auto_captions},
  1406.        'copy-caption|copy_caption!'     => \$opt{copy_caption},
  1407.        'captions-dir|captions_dir=s'    => \$opt{captions_dir},
  1408.        'skip-if-exists|skip_if_exists!' => \$opt{skip_if_exists},
  1409.        'downloads-dir|download-dir=s'   => \$opt{downloads_dir},
  1410.        'fat32safe!'                     => \$opt{fat32safe},
  1411.      )
  1412.      or warn "[!] Error in command-line arguments!\n";
  1413.  
  1414.    apply_configuration(\%opt, $keywords);
  1415. }
  1416.  
  1417. # Parse the arguments
  1418. if (@ARGV) {
  1419.    require Encode;
  1420.    @ARGV = map { Encode::decode_utf8($_) } @ARGV;
  1421.    parse_arguments(\@ARGV);
  1422. }
  1423.  
  1424. for (my $i = 0 ; $i <= $#ARGV ; $i++) {
  1425.    my $arg = $ARGV[$i];
  1426.  
  1427.    next if chr ord $arg eq q{-};
  1428.  
  1429.    if (youtube_urls($arg)) {
  1430.        splice(@ARGV, $i--, 1);
  1431.    }
  1432. }
  1433.  
  1434. if (my @keywords = grep chr ord ne q{-}, @ARGV) {
  1435.    print_videos($yv_obj->search_videos(\@keywords));
  1436. }
  1437. elsif ($opt{interactive} and -t) {
  1438.    first_user_input();
  1439. }
  1440. elsif ($opt{interactive} and -t STDOUT and not -t) {
  1441.    print_videos($yv_obj->search_videos(scalar <STDIN>));
  1442. }
  1443. else {
  1444.    main_quit($opt{_error} || 0);
  1445. }
  1446.  
  1447. sub get_valid_video_id {
  1448.    my ($value) = @_;
  1449.  
  1450.    my $id =
  1451.        $value =~ /$get_video_id_re/   ? $+{video_id}
  1452.      : $value =~ /$valid_video_id_re/ ? $value
  1453.      :                                  undef;
  1454.  
  1455.    unless (defined $id) {
  1456.        warn_invalid('videoID', $value);
  1457.        return;
  1458.    }
  1459.  
  1460.    return $id;
  1461. }
  1462.  
  1463. sub get_valid_playlist_id {
  1464.    my ($value) = @_;
  1465.  
  1466.    my $id =
  1467.        $value =~ /$get_playlist_id_re/   ? $+{playlist_id}
  1468.      : $value =~ /$valid_playlist_id_re/ ? $value
  1469.      :                                     undef;
  1470.  
  1471.    unless (defined $id) {
  1472.        warn_invalid('playlistID', $value);
  1473.        return;
  1474.    }
  1475.  
  1476.    return $id;
  1477. }
  1478.  
  1479. sub apply_input_arguments {
  1480.    my ($args, $keywords) = @_;
  1481.  
  1482.    if (@{$args}) {
  1483.        local @ARGV = @{$args};
  1484.        parse_arguments($keywords);
  1485.    }
  1486.  
  1487.    return 1;
  1488. }
  1489.  
  1490. # Get mplayer
  1491. sub get_mplayer {
  1492.    if ($constant{win32}) {
  1493.        my $smplayer = catfile($ENV{ProgramFiles}, qw(SMPlayer mplayer mplayer.exe));
  1494.  
  1495.        if (not -e $smplayer) {
  1496.            warn "\n\n!!! Please install SMPlayer in order to stream YouTube videos.\n\n";
  1497.        }
  1498.  
  1499.        return $smplayer;    # Windows MPlayer
  1500.    }
  1501.  
  1502.    return 'mplayer';        # *NIX MPlayer
  1503. }
  1504.  
  1505. # Get term width
  1506. sub get_term_width {
  1507.    return $term_width if $constant{win32};
  1508.    $term_width = (-t STDOUT) ? ((split(q{ }, `stty size`))[1] || $term_width) : $term_width;
  1509. }
  1510.  
  1511. sub first_user_input {
  1512.    my @keys = get_input_for_first_time();
  1513.  
  1514.    state $first_input_help = <<"HELP";
  1515.  
  1516. $base_options
  1517. $action_options
  1518. $other_options
  1519. $notes_options
  1520. ** Example:
  1521.    To search for playlists, insert: -p keywords
  1522. HELP
  1523.  
  1524.    if (scalar(@keys)) {
  1525.        my @for_search;
  1526.        foreach my $key (@keys) {
  1527.            if ($key =~ /$valid_opt_re/) {
  1528.  
  1529.                my $opt = $1;
  1530.  
  1531.                if (general_options(opt => $opt)) {
  1532.                    ## ok
  1533.                }
  1534.                elsif ($opt =~ /^(?:h|help)\z/) {
  1535.                    print $first_input_help;
  1536.                    press_enter_to_continue();
  1537.                }
  1538.                elsif ($opt =~ /^(?:r|return)\z/) {
  1539.                    return;
  1540.                }
  1541.                else {
  1542.                    warn_invalid('option', $opt);
  1543.                    print "\n";
  1544.                    exit 1;
  1545.                }
  1546.            }
  1547.            elsif (youtube_urls($key)) {
  1548.                ## ok
  1549.            }
  1550.            else {
  1551.                push @for_search, $key;
  1552.            }
  1553.        }
  1554.  
  1555.        if (scalar(@for_search) > 0) {
  1556.            print_videos($yv_obj->search_videos(\@for_search));
  1557.        }
  1558.        else {
  1559.            __SUB__->();
  1560.        }
  1561.    }
  1562.    else {
  1563.        __SUB__->();
  1564.    }
  1565. }
  1566.  
  1567. sub get_quotewords {
  1568.    require Text::ParseWords;
  1569.    Text::ParseWords::quotewords(@_);
  1570. }
  1571.  
  1572. # Straight copy of parse_options() from Term::UI
  1573. sub _parse_options {
  1574.    my ($input) = @_;
  1575.  
  1576.    my $return = {};
  1577.    while (   $input =~ s/(?:^|\s+)--?([-\w]+=(["']).+?\2)(?=\Z|\s+)//
  1578.           or $input =~ s/(?:^|\s+)--?([-\w]+=\S+)(?=\Z|\s+)//
  1579.           or $input =~ s/(?:^|\s+)--?([-\w]+)(?=\Z|\s+)//) {
  1580.        my $match = $1;
  1581.  
  1582.        if ($match =~ /^([-\w]+)=(["'])(.+?)\2$/) {
  1583.             $return->{$1} = $3;
  1584.  
  1585.         }
  1586.         elsif ($match =~ /^([-\w]+)=(\S+)$/) {
  1587.             $return->{$1} = $2;
  1588.  
  1589.         }
  1590.         elsif ($match =~ /^no-?([-\w]+)$/i) {
  1591.             $return->{$1} = 0;
  1592.  
  1593.         }
  1594.         elsif ($match =~ /^([-\w]+)$/) {
  1595.             $return->{$1} = 1;
  1596.         }
  1597.     }
  1598.  
  1599.     return wantarray ? ($return, $input) : $return;
  1600. }
  1601.  
  1602. sub parse_options2 {
  1603.     my ($input) = @_;
  1604.  
  1605.     warn(colored("\n[!] Input with an odd number of quotes: <$input>", 'bold red') . "\n\n")
  1606.       if $yv_obj->get_debug;
  1607.  
  1608.     my ($args, $keywords) = _parse_options($input);
  1609.  
  1610.     my @args =
  1611.         map $args->{$_} eq '0' ? "--no-$_"
  1612.       : $args->{$_} eq '1'     ? "--$_"
  1613.       :                          "--$_=$args->{$_}" => keys %{$args};
  1614.  
  1615.     return wantarray ? (\@args, [split q{ }, $keywords]) : \@args;
  1616. }
  1617.  
  1618. sub parse_options {
  1619.     my ($input) = @_;
  1620.     my (@args, @keywords);
  1621.  
  1622.     if (not defined($input) or $input eq q{}) {
  1623.         return \@args, \@keywords;
  1624.     }
  1625.  
  1626.     foreach my $word (get_quotewords(qr/\s+/, 1, $input)) {
  1627.         if (chr ord $word eq q{-}) {
  1628.             push @args, $word;
  1629.         }
  1630.         else {
  1631.             push @keywords, $word;
  1632.         }
  1633.     }
  1634.  
  1635.     if (not @args and not @keywords) {
  1636.         return parse_options2($input);
  1637.     }
  1638.  
  1639.     return wantarray ? (\@args, \@keywords) : \@args;
  1640. }
  1641.  
  1642. sub get_user_input {
  1643.     my ($text) = @_;
  1644.  
  1645.     if (not $opt{interactive}) {
  1646.         if (not defined $opt{std_input}) {
  1647.             return ':return';
  1648.         }
  1649.     }
  1650.  
  1651.     my $input = unpack(
  1652.                        'A*', defined($opt{std_input})
  1653.                        ? delete($opt{std_input})
  1654.                        : ($term->readline($text) // return ':return')
  1655.                       ) =~ s/^\s+//r;
  1656.  
  1657.     return q{:next} if $input eq q{};    # <ENTER> for the next page
  1658.  
  1659.     require Encode;
  1660.     $input = Encode::decode_utf8($input);
  1661.  
  1662.     my ($args, $keywords) = parse_options($input);
  1663.  
  1664.     if ($opt{history} and @{$keywords}) {
  1665.         my $str = join(' ', grep { /\w/ and not /^[:;=]/ } @{$keywords});
  1666.         if ($str ne '' and $str !~ /^[0-9]{1,2}\z/) {
  1667.             $term->append_history(1, $opt{history_file});
  1668.         }
  1669.     }
  1670.  
  1671.     apply_input_arguments($args, $keywords);
  1672.     return @{$keywords};
  1673. }
  1674.  
  1675. sub logout {
  1676.  
  1677.     unlink $authentication_file
  1678.       or warn "Can't unlink: `$authentication_file' -> $!";
  1679.  
  1680.    $yv_obj->set_access_token();
  1681.    $yv_obj->set_refresh_token();
  1682.  
  1683.    return 1;
  1684. }
  1685.  
  1686. sub authenticate {
  1687.    my $get_code_url = $yv_obj->get_accounts_oauth_url() // return;
  1688.  
  1689.    print <<"INFO";
  1690.  
  1691. [*] Get the authentication code: $get_code_url
  1692.  
  1693.                            |
  1694. ... and paste it below.    \\|/
  1695.                            `
  1696. INFO
  1697.  
  1698.    my $code = $term->readline(colored(q{Code: }, 'bold')) || return;
  1699.  
  1700.    my $info = $yv_obj->oauth_login($code) // do {
  1701.        warn "[WARNING] Can't log in... That's all I know...\n";
  1702.        return;
  1703.    };
  1704.  
  1705.    if (defined $info->{access_token}) {
  1706.  
  1707.        $yv_obj->set_access_token($info->{access_token}) // return;
  1708.        $yv_obj->set_refresh_token($info->{refresh_token}) // return;
  1709.  
  1710.        my $remember_me = ask_yn(prompt  => colored("\nRemember me", 'bold'),
  1711.                                 default => 'y');
  1712.  
  1713.        if ($remember_me) {
  1714.            $yv_obj->set_authentication_file($authentication_file);
  1715.            $yv_obj->save_authentication_tokens()
  1716.              or warn "Can't store the authentication tokens: $!";
  1717.        }
  1718.        else {
  1719.            $yv_obj->set_authentication_file();
  1720.        }
  1721.  
  1722.        return 1;
  1723.    }
  1724.  
  1725.    warn "[WARNING] There was a problem with the authentication...\n";
  1726.    return;
  1727. }
  1728.  
  1729. sub authenticated {
  1730.    if (not defined $yv_obj->get_access_token) {
  1731.        warn_needs_auth();
  1732.        return;
  1733.    }
  1734.    return 1;
  1735. }
  1736.  
  1737. sub favorite_videos {
  1738.    my (@videoIDs) = @_;
  1739.    return if not authenticated();
  1740.  
  1741.    foreach my $id (@videoIDs) {
  1742.        my $videoID = get_valid_video_id($id) // next;
  1743.  
  1744.        if ($yv_obj->favorite_video($videoID)) {
  1745.            printf "\n[*] Video %s has been successfully favorited.\n", sprintf($CONFIG{youtube_video_url}, $videoID);
  1746.        }
  1747.        else {
  1748.            warn_cant_do('favorite', $videoID);
  1749.        }
  1750.    }
  1751.    return 1;
  1752. }
  1753.  
  1754. sub select_and_save_to_playlist {
  1755.    return if not authenticated();
  1756.  
  1757.    my $request = $yv_obj->my_playlists() // last;
  1758.    my $playlistID = print_playlists($request, return_playlist_id => 1);
  1759.  
  1760.    if (defined($playlistID)) {
  1761.        return save_to_playlist($playlistID, @_);
  1762.    }
  1763.  
  1764.    warn_no_thing_selected('playlist');
  1765.    return;
  1766.  
  1767. }
  1768.  
  1769. sub save_to_playlist {
  1770.    my ($playlistID, @videoIDs) = @_;
  1771.    return if not authenticated();
  1772.  
  1773.    foreach my $id (@videoIDs) {
  1774.        my $videoID = get_valid_video_id($id) // next;
  1775.  
  1776.        if ($yv_obj->add_video_to_playlist($playlistID, $videoID, $opt{position} || 1)) {
  1777.            printf("\n[*] Video %s has been successfully added to playlistID: %s\n",
  1778.                   sprintf($CONFIG{youtube_video_url}, $videoID), $playlistID);
  1779.        }
  1780.        else {
  1781.            warn_cant_do("add to playlist", $videoID);
  1782.        }
  1783.    }
  1784.    return 1;
  1785. }
  1786.  
  1787. sub rate_videos {
  1788.    my $rating = shift;
  1789.    return if not authenticated();
  1790.  
  1791.    foreach my $id (@_) {
  1792.        my $videoID = get_valid_video_id($id) // next;
  1793.        if ($yv_obj->send_rating_to_video($videoID, $rating)) {
  1794.            print "\n[*] VideoID '$videoID' has been successfully ${rating}d.\n";
  1795.        }
  1796.        else {
  1797.            warn colored("\n[!] VideoID '$videoID' has not been ${rating}d", 'bold red') . "\n";
  1798.        }
  1799.    }
  1800.    return 1;
  1801. }
  1802.  
  1803. sub get_and_play_video_ids {
  1804.    my @ids = grep { get_valid_video_id($_) } @_;
  1805.  
  1806.    if (not @ids) {
  1807.        warn_invalid('video IDs', "@_");
  1808.        return;
  1809.    }
  1810.  
  1811.    my $info = $yv_obj->video_details(join(',', @ids), VIDEO_PART);
  1812.  
  1813.    if ($yv_utils->has_entries($info)) {
  1814.        if (not play_videos($info->{results}{items})) {
  1815.            return;
  1816.        }
  1817.    }
  1818.    else {
  1819.        warn_cant_do('get info about', "@ids");
  1820.    }
  1821.  
  1822.    return 1;
  1823. }
  1824.  
  1825. sub get_and_play_playlists {
  1826.    foreach my $id (@_) {
  1827.        my $videos = $yv_obj->videos_from_playlist_id(get_valid_playlist_id($id) // next);
  1828.        local $opt{play_all} = length($opt{std_input}) ? 0 : 1;
  1829.        print_videos($videos, auto => $opt{play_all});
  1830.    }
  1831.    return 1;
  1832. }
  1833.  
  1834. sub get_and_print_video_info {
  1835.    foreach my $id (@_) {
  1836.  
  1837.        my $videoID = get_valid_video_id($id) // next;
  1838.        my $info = $yv_obj->video_details($videoID, VIDEO_PART);
  1839.  
  1840.        if ($yv_utils->has_entries($info)) {
  1841.            print_video_info($info->{results}{items}[0]);
  1842.        }
  1843.        else {
  1844.            warn_cant_get('information', $videoID);
  1845.        }
  1846.    }
  1847.    return 1;
  1848. }
  1849.  
  1850. sub get_and_print_related_videos {
  1851.    foreach my $id (@_) {
  1852.        my $videoID = get_valid_video_id($id) // next;
  1853.        my $results = $yv_obj->related_to_videoID($videoID);
  1854.        print_videos($results);
  1855.    }
  1856.    return 1;
  1857. }
  1858.  
  1859. sub get_and_print_comments {
  1860.    foreach my $id (@_) {
  1861.        my $videoID  = get_valid_video_id($id) // next;
  1862.        my $comments = $yv_obj->comments_from_video_id($videoID);
  1863.        print_comments($comments, $videoID);
  1864.    }
  1865.    return 1;
  1866. }
  1867.  
  1868. sub get_and_print_videos_from_playlist {
  1869.    my ($playlistID) = @_;
  1870.  
  1871.    if ($playlistID =~ /$valid_playlist_id_re/) {
  1872.        my $info = $yv_obj->videos_from_playlist_id($playlistID);
  1873.        if ($yv_utils->has_entries($info)) {
  1874.            print_videos($info);
  1875.        }
  1876.        else {
  1877.            warn colored("\n[!] Inexistent playlist...", 'bold red') . "\n";
  1878.            return;
  1879.        }
  1880.    }
  1881.    else {
  1882.        warn_invalid('playlistID', $playlistID);
  1883.        return;
  1884.    }
  1885.    return 1;
  1886. }
  1887.  
  1888. sub subscribe_to {
  1889.    my ($is_channel, @ids) = @_;
  1890.  
  1891.    return if not authenticated();
  1892.  
  1893.    foreach my $channel (@ids) {
  1894.        if ($channel =~ /$valid_channel_id_re/) {
  1895.            if (
  1896.                  $is_channel
  1897.                ? $yv_obj->subscribe_channel($+{channel_id})
  1898.                : $yv_obj->subscribe_channel_from_username($+{channel_id})
  1899.              ) {
  1900.                print "[*] Successfully subscribed to channel: $channel\n";
  1901.            }
  1902.            else {
  1903.                warn colored("\n[!] Unable to subscribe to channel: $channel", 'bold red') . "\n";
  1904.            }
  1905.        }
  1906.    }
  1907.    return 1;
  1908. }
  1909.  
  1910. sub subscribe_to_channels {
  1911.    subscribe_to(1, @_);
  1912. }
  1913.  
  1914. sub subscribe_to_usernames {
  1915.    subscribe_to(0, @_);
  1916. }
  1917.  
  1918. sub _bold_color {
  1919.    my ($text) = @_;
  1920.    return colored($text, 'bold');
  1921. }
  1922.  
  1923. sub youtube_urls {
  1924.    my ($arg) = @_;
  1925.  
  1926.    if ($arg =~ /$get_video_id_re/) {
  1927.        get_and_play_video_ids($+{video_id});
  1928.    }
  1929.    elsif ($arg =~ /$get_playlist_id_re/) {
  1930.        get_and_print_videos_from_playlist($+{playlist_id});
  1931.    }
  1932.    elsif ($arg =~ /$get_channel_playlists_id_re/) {
  1933.        print_playlists($yv_obj->playlists($+{channel_id}));
  1934.    }
  1935.    elsif ($arg =~ /$get_channel_videos_id_re/) {
  1936.        print_videos($yv_obj->uploads($+{channel_id}));
  1937.    }
  1938.    elsif ($arg =~ /$get_username_playlists_re/) {
  1939.        print_playlists($yv_obj->playlists_from_username($+{username}));
  1940.    }
  1941.    elsif ($arg =~ /$get_username_videos_re/) {
  1942.        print_videos($yv_obj->uploads_from_username($+{username}));
  1943.    }
  1944.    else {
  1945.        return;
  1946.    }
  1947.  
  1948.    return 1;
  1949. }
  1950.  
  1951. sub general_options {
  1952.    my %args = @_;
  1953.  
  1954.    my $url      = $args{url};
  1955.    my $option   = $args{opt};
  1956.    my $callback = $args{sub};
  1957.    my $results  = $args{res};
  1958.    my $info     = $args{info};
  1959.  
  1960.    if (not defined($option)) {
  1961.        return;
  1962.    }
  1963.  
  1964.    if ($option =~ /^(?:q|quit|exit)\z/) {
  1965.        main_quit(0);
  1966.    }
  1967.    elsif ($option =~ /^(?:n|next)\z/ and defined $url) {
  1968.        if (defined $info->{nextPageToken}) {
  1969.            my $request = $yv_obj->next_page($url, $info->{nextPageToken});
  1970.            $callback->($request);
  1971.        }
  1972.        else {
  1973.            warn_last_page();
  1974.        }
  1975.    }
  1976.    elsif ($option =~ /^(?:R|refresh)\z/ and defined $url) {
  1977.        @{$results} = @{$yv_obj->_get_results($url)->{results}{items}};
  1978.    }
  1979.    elsif ($option =~ /^(?:b|back|p|prev|previous)\z/ and defined $url) {
  1980.        if (defined $info->{prevPageToken}) {
  1981.            my $request = $yv_obj->previous_page($url, $info->{prevPageToken});
  1982.            $callback->($request);
  1983.        }
  1984.        else {
  1985.            warn_first_page();
  1986.        }
  1987.    }
  1988.    elsif ($option eq 'login') {
  1989.        authenticate();
  1990.    }
  1991.    elsif ($option eq 'logout') {
  1992.        logout();
  1993.    }
  1994.    elsif ($option =~ /^(?:reset|reload|restart)\z/) {
  1995.        @ARGV = ();
  1996.        do $0;
  1997.    }
  1998.    elsif ($option =~ /^dv${digit_or_equal_re}(.*)/ and ref($results) eq 'ARRAY') {
  1999.        if (my @nums = get_valid_numbers($#{$results}, $1)) {
  2000.            print "\n";
  2001.            foreach my $num (@nums) {
  2002.                require Data::Dump;
  2003.                say Data::Dump::pp($results->[$num]);
  2004.            }
  2005.            press_enter_to_continue();
  2006.        }
  2007.        else {
  2008.            warn_no_thing_selected('result');
  2009.        }
  2010.    }
  2011.    elsif ($option =~ /^v(?:ideoids?)?=(.*)/) {
  2012.        if (my @ids = split(/[,\s]+/, $1)) {
  2013.            get_and_play_video_ids(@ids);
  2014.        }
  2015.        else {
  2016.            warn colored("\n[!] No video ID specified!", 'bold red') . "\n";
  2017.        }
  2018.    }
  2019.    elsif ($option =~ /^playlist(?:ID)?=(.*)/) {
  2020.        get_and_print_videos_from_playlist($1);
  2021.    }
  2022.    else {
  2023.        return;
  2024.    }
  2025.  
  2026.    return 1;
  2027. }
  2028.  
  2029. sub warn_no_results {
  2030.    warn colored("\n[!] No $_[0] results!", 'bold red') . "\n";
  2031. }
  2032.  
  2033. sub warn_invalid {
  2034.    my ($name, $option) = @_;
  2035.    warn colored("\n[!] Invalid $name: <$option>", 'bold red') . "\n";
  2036. }
  2037.  
  2038. sub warn_cant_do {
  2039.    my ($action, $videoID) = @_;
  2040.    warn colored("\n[!] Can't $action video: " . sprintf($CONFIG{youtube_video_url}, $videoID), 'bold red') . "\n";
  2041. }
  2042.  
  2043. sub warn_cant_get {
  2044.    my ($name, $videoID) = @_;
  2045.    warn colored("\n[!] Can't get $name for video: " . sprintf($CONFIG{youtube_video_url}, $videoID), 'bold red') . "\n";
  2046. }
  2047.  
  2048. sub warn_last_page {
  2049.    warn colored("\n[!] This is the last page!", "bold red") . "\n";
  2050. }
  2051.  
  2052. sub warn_first_page {
  2053.    warn colored("\n[!] No previous page available...", 'bold red') . "\n";
  2054. }
  2055.  
  2056. sub warn_no_thing_selected {
  2057.    warn colored("\n[!] No $_[0] selected!", 'bold red') . "\n";
  2058. }
  2059.  
  2060. sub warn_needs_auth {
  2061.    warn colored("\n[!] This functionality needs authentication!", 'bold red') . "\n";
  2062. }
  2063.  
  2064. # ... GET INPUT SUBS ... #
  2065. sub get_input_for_first_time {
  2066.    return get_user_input(_bold_color("\n=>> Search for YouTube videos (:h for help)") . "\n> ");
  2067. }
  2068.  
  2069. sub get_input_for_channels {
  2070.    return get_user_input(_bold_color("\n=>> Select a channel (:h for help)") . "\n> ");
  2071. }
  2072.  
  2073. sub get_input_for_search {
  2074.    return get_user_input(_bold_color("\n=>> Select one or more videos to play (:h for help)") . "\n> ");
  2075. }
  2076.  
  2077. sub get_input_for_playlists {
  2078.    return get_user_input(_bold_color("\n=>> Select a playlist (:h for help)") . "\n> ");
  2079. }
  2080.  
  2081. sub get_input_for_comments {
  2082.    return get_user_input(_bold_color("\n=>> Press <ENTER> for the next page of comments (:h for help)") . "\n> ");
  2083. }
  2084.  
  2085. sub get_input_for_categories {
  2086.    return get_user_input(_bold_color("\n=>> Select a category (:h for help)") . "\n> ");
  2087. }
  2088.  
  2089. sub ask_yn {
  2090.    my (%opt) = @_;
  2091.    my $c = join('/', map { $_ eq $opt{default} ? ucfirst($_) : $_ } qw(y n));
  2092.  
  2093.    my $answ;
  2094.    do {
  2095.        $answ = lc($term->readline($opt{prompt} . " [$c]: "));
  2096.        $answ = $opt{default} unless $answ =~ /\S/;
  2097.    } while ($answ !~ /^y(?:es)?$/ and $answ !~ /^no?$/);
  2098.  
  2099.    return chr(ord($answ)) eq 'y';
  2100. }
  2101.  
  2102. sub get_reply {
  2103.    my (%opt) = @_;
  2104.  
  2105.    my $default = 1;
  2106.    while (my ($i, $choice) = each @{$opt{choices}}) {
  2107.        print "\n" if $i == 0;
  2108.        printf("%3d> %s\n", $i + 1, $choice);
  2109.        if ($choice eq $opt{default}) {
  2110.            $default = $i + 1;
  2111.        }
  2112.    }
  2113.    print "\n";
  2114.  
  2115.    my $answ;
  2116.    do {
  2117.        $answ = $term->readline($opt{prompt} . " [$default]: ");
  2118.        $answ = $default unless $answ =~ /\S/;
  2119.    } while ($answ !~ /^[0-9]+\z/ or $answ < 1 or $answ > @{$opt{choices}});
  2120.  
  2121.    return $opt{choices}[$answ - 1];
  2122. }
  2123.  
  2124. sub valid_num {
  2125.    my ($num, $array_ref) = @_;
  2126.    return $num =~ /^[0-9]{1,2}\z/ && $num != 0 && $num <= @{$array_ref};
  2127. }
  2128.  
  2129. sub adj_width {
  2130.    my ($str, $len, $prepend) = @_;
  2131.  
  2132.    $len > 0 or do {
  2133.        warn "[WARN] Insufficient space for the title: increase your terminal width!\n";
  2134.        return $str;
  2135.    };
  2136.  
  2137.    state $pkg = (
  2138.        eval {
  2139.            require Unicode::GCString;
  2140.            'Unicode::GCString';
  2141.        } // eval {
  2142.            require Text::CharWidth;
  2143.            'Text::CharWidth';
  2144.        } // do {
  2145.            warn "[WARN] Please install Unicode::GCString or Text::CharWidth in order to use this functionality.\n";
  2146.            '';
  2147.          }
  2148.    );
  2149.  
  2150.    #
  2151.    ## Unicode::GCString
  2152.    #
  2153.    if ($pkg eq 'Unicode::GCString') {
  2154.  
  2155.        my $gcstr     = Unicode::GCString->new($str);
  2156.        my $str_width = $gcstr->columns;
  2157.  
  2158.        if ($str_width != $len) {
  2159.            while ($str_width > $len) {
  2160.                $gcstr = $gcstr->substr(0, -1);
  2161.                $str_width = $gcstr->columns;
  2162.            }
  2163.  
  2164.            $str = $gcstr->as_string;
  2165.            my $spaces = ' ' x ($len - $str_width);
  2166.            $str = $prepend ? "$spaces$str" : "$str$spaces";
  2167.        }
  2168.  
  2169.        return $str;
  2170.    }
  2171.  
  2172.    #
  2173.    ## Text::CharWidth
  2174.    #
  2175.    if ($pkg eq 'Text::CharWidth') {
  2176.  
  2177.        my $str_width = Text::CharWidth::mbswidth($str);
  2178.  
  2179.        if ($str_width != $len) {
  2180.            while ($str_width > $len) {
  2181.                chop $str;
  2182.                $str_width = Text::CharWidth::mbswidth($str);
  2183.            }
  2184.  
  2185.            my $spaces = ' ' x ($len - $str_width);
  2186.            $str = $prepend ? "$spaces$str" : "$str$spaces";
  2187.        }
  2188.  
  2189.        return $str;
  2190.    }
  2191.  
  2192.    return $str;
  2193. }
  2194.  
  2195. # ... PRINT SUBROUTINES ... #
  2196. sub print_channels {
  2197.    my ($results) = @_;
  2198.  
  2199.    if (not $yv_utils->has_entries($results)) {
  2200.        warn_no_results("channel");
  2201.    }
  2202.  
  2203.    if ($opt{get_term_width} and $opt{results_fixed_width}) {
  2204.        get_term_width();
  2205.    }
  2206.  
  2207.    my $url      = $results->{url};
  2208.    my $info     = $results->{results} // {};
  2209.    my $channels = $info->{items} // [];
  2210.  
  2211.    my $i = 0;
  2212.    foreach my $channel (@{$channels}) {
  2213.  
  2214.        if ($opt{results_with_details}) {
  2215.            printf(
  2216.                   "\n%s. %s\n    %s: %-23s %s: %-12s\n%s\n",
  2217.                   colored(sprintf('%2d', ++$i), 'bold') => colored($yv_utils->get_title($channel), 'bold blue'),
  2218.                   colored('Updated' => 'bold') => $yv_utils->get_publication_date($channel),
  2219.                   colored('Author'  => 'bold') => $yv_utils->get_channel_title($channel),
  2220.                   wrap_text(
  2221.                             i_tab => q{ } x 4,
  2222.                             s_tab => q{ } x 4,
  2223.                             text  => [$yv_utils->get_description($channel) || 'No description available...']
  2224.                            ),
  2225.                  );
  2226.        }
  2227.        elsif ($opt{results_fixed_width}) {
  2228.  
  2229.            require List::Util;
  2230.  
  2231.            my @authors = map { $yv_utils->get_channel_title($_) } @{$channels};
  2232.            my @dates   = map { $yv_utils->get_publication_date($_) } @{$channels};
  2233.  
  2234.            my $author_width = List::Util::min(List::Util::max(map { length($_) } @authors), int($term_width / 5));
  2235.            my $dates_width = List::Util::max(map { length($_) } @dates);
  2236.            my $title_length = $term_width - ($author_width + $dates_width + 2 + 3 + 1 + 2);
  2237.  
  2238.            print "\n";
  2239.            foreach my $i (0 .. $#{$channels}) {
  2240.                my $channel = $channels->[$i];
  2241.                printf "%s. %s %s [%*s]\n", colored(sprintf('%2d', $i + 1), 'bold'),
  2242.                  adj_width($yv_utils->get_title($channel), $title_length),
  2243.                  adj_width($authors[$i], $author_width, 1),
  2244.                  $dates_width, $dates[$i];
  2245.            }
  2246.            last;
  2247.        }
  2248.        else {
  2249.            print "\n" if $i == 0;
  2250.            printf "%s. %s (by %s)\n", colored(sprintf('%2d', ++$i), 'bold'), $yv_utils->get_title($channel),
  2251.              $yv_utils->get_channel_title($channel);
  2252.        }
  2253.    }
  2254.  
  2255.    my @keywords = get_input_for_channels();
  2256.  
  2257.    my @for_search;
  2258.    foreach my $key (@keywords) {
  2259.        if ($key =~ /$valid_opt_re/) {
  2260.  
  2261.            my $opt = $1;
  2262.  
  2263.            if (
  2264.                general_options(
  2265.                                opt  => $opt,
  2266.                                sub  => __SUB__,
  2267.                                url  => $url,
  2268.                                res  => $channels,
  2269.                                info => $info,
  2270.                               )
  2271.              ) {
  2272.                ## ok
  2273.            }
  2274.            elsif ($opt =~ /^(?:h|help)\z/) {
  2275.                print $general_help;
  2276.                press_enter_to_continue();
  2277.            }
  2278.            elsif ($opt =~ /^(?:r|return)\z/) {
  2279.                return;
  2280.            }
  2281.            else {
  2282.                warn_invalid('option', $opt);
  2283.            }
  2284.        }
  2285.        elsif (youtube_urls($key)) {
  2286.            ## ok
  2287.        }
  2288.        elsif (valid_num($key, $channels)) {
  2289.            print_videos($yv_obj->uploads($yv_utils->get_channel_id($channels->[$key - 1])));
  2290.        }
  2291.        else {
  2292.            push @for_search, $key;
  2293.        }
  2294.    }
  2295.  
  2296.    if (@for_search) {
  2297.        __SUB__->($yv_obj->search_channels(\@for_search));
  2298.    }
  2299.  
  2300.    __SUB__->(@_);
  2301. }
  2302.  
  2303. sub print_comments {
  2304.    my ($results, $videoID) = @_;
  2305.  
  2306.    if (not $yv_utils->has_entries($results)) {
  2307.        warn_no_results("comments");
  2308.    }
  2309.  
  2310.    my $url      = $results->{url};
  2311.    my $info     = $results->{results} // {};
  2312.    my $comments = $info->{items} // [];
  2313.  
  2314.    my $i = 0;
  2315.    foreach my $comment (@{$comments}) {
  2316.        my $snippet = (($comment->{snippet} // next)->{topLevelComment} // next)->{snippet};
  2317.  
  2318.        printf(
  2319.               "\n%s on %s said:\n%s\n",
  2320.               colored($snippet->{authorDisplayName}, 'bold'),
  2321.               $yv_utils->format_date($snippet->{publishedAt}),
  2322.               wrap_text(
  2323.                         i_tab => q{ } x 4,
  2324.                         s_tab => q{ } x 4,
  2325.                         text  => [$snippet->{textDisplay} // 'Empty comment...']
  2326.                        ),
  2327.              );
  2328.    }
  2329.  
  2330.    my @keywords = get_input_for_comments();
  2331.  
  2332.    foreach my $key (@keywords) {
  2333.        if ($key =~ /$valid_opt_re/) {
  2334.  
  2335.            my $opt = $1;
  2336.  
  2337.            if (
  2338.                general_options(
  2339.                                opt  => $opt,
  2340.                                sub  => __SUB__,
  2341.                                url  => $url,
  2342.                                res  => $comments,
  2343.                                info => $info,
  2344.                                mode => 'comments',
  2345.                                args => [$videoID],
  2346.                               )
  2347.              ) {
  2348.                ## ok
  2349.            }
  2350.            elsif ($opt =~ /^(?:h|help)\z/) {
  2351.                print $comments_help;
  2352.                press_enter_to_continue();
  2353.            }
  2354.            elsif ($opt =~ /^(?:c|comment)\z/) {
  2355.                if (authenticated()) {
  2356.                    require File::Temp;
  2357.                    my ($fh, $filename) = File::Temp::tempfile();
  2358.                    $yv_obj->proxy_system($ENV{EDITOR} // 'nano', $filename);
  2359.                    if ($?) {
  2360.                        warn colored("\n[!] Editor exited with a non-zero code. Unable to continue!", 'bold red') . "\n";
  2361.                    }
  2362.                    else {
  2363.                        my $comment = do { local (@ARGV, $/) = $filename; <> };
  2364.                        $comment =~ s/[^\s[:^cntrl:]]+//g;    # remove control characters
  2365.  
  2366.                        if (length($comment) and $yv_obj->comment_to_video_id($comment, $videoID)) {
  2367.                            print "\n[*] Comment posted!\n";
  2368.                        }
  2369.                        else {
  2370.                            warn colored("\n[!] Your comment has NOT been posted!", 'bold red') . "\n";
  2371.                        }
  2372.                    }
  2373.                }
  2374.            }
  2375.            elsif ($opt =~ /^(?:r|return)\z/) {
  2376.                return;
  2377.            }
  2378.            else {
  2379.                warn_invalid('option', $opt);
  2380.            }
  2381.        }
  2382.        elsif (youtube_urls($key)) {
  2383.            ## ok
  2384.        }
  2385.        elsif (valid_num($key, $comments)) {
  2386.            print_videos($yv_obj->get_videos_from_username($comments->[$key - 1]{author}));
  2387.        }
  2388.        else {
  2389.            warn_invalid('keyword', $key);
  2390.        }
  2391.    }
  2392.  
  2393.    __SUB__->(@_);
  2394. }
  2395.  
  2396. sub print_categories {
  2397.    my ($results) = @_;
  2398.  
  2399.    my $categories = $results->{items};
  2400.    return if ref $categories ne 'ARRAY';
  2401.  
  2402.    my $i = 0;
  2403.    print "\n" if @{$categories};
  2404.  
  2405.    foreach my $category (@{$categories}) {
  2406.  
  2407.        # Ignore nonassignable categories
  2408.        $category->{snippet}{assignable} || next;
  2409.  
  2410.        printf "%s. %-40s (id: %s)\n", colored(sprintf('%2d', ++$i), 'bold'), $yv_utils->get_title($category), $category->{id};
  2411.    }
  2412.  
  2413.    my @keywords = get_input_for_categories();
  2414.  
  2415.    foreach my $key (@keywords) {
  2416.        if ($key =~ /$valid_opt_re/) {
  2417.  
  2418.            my $opt = $1;
  2419.  
  2420.            if (
  2421.                general_options(
  2422.                                opt => $opt,
  2423.                                sub => __SUB__,
  2424.                                res => $results,
  2425.                               )
  2426.              ) {
  2427.                ## ok
  2428.            }
  2429.            elsif ($opt =~ /^(?:h|help)\z/) {
  2430.                print $general_help;
  2431.                press_enter_to_continue();
  2432.            }
  2433.            elsif ($opt =~ /^(?:r|return)\z/) {
  2434.                return;
  2435.            }
  2436.            else {
  2437.                warn_invalid('option', $opt);
  2438.            }
  2439.        }
  2440.        elsif (youtube_urls($key)) {
  2441.            ## ok
  2442.        }
  2443.        elsif (valid_num($key, $categories)) {
  2444.            my $cat_id = $categories->[$key - 1]{id};
  2445.            print_videos($yv_obj->videos_from_category($cat_id));
  2446.        }
  2447.        else {
  2448.            warn_invalid('keyword', $key);
  2449.        }
  2450.    }
  2451.  
  2452.    __SUB__->(@_);
  2453. }
  2454.  
  2455. sub print_playlists {
  2456.    my ($results, %args) = @_;
  2457.  
  2458.    if (not $yv_utils->has_entries($results)) {
  2459.        warn_no_results("playlist");
  2460.    }
  2461.  
  2462.    if ($opt{get_term_width} and $opt{results_fixed_width}) {
  2463.        get_term_width();
  2464.    }
  2465.  
  2466.    my $url       = $results->{url};
  2467.    my $info      = $results->{results} // {};
  2468.    my $playlists = $info->{items} // [];
  2469.  
  2470.    if ($opt{shuffle}) {
  2471.        require List::Util;
  2472.        $playlists = [List::Util::shuffle(@{$playlists})];
  2473.    }
  2474.  
  2475.    state $info_format = <<"FORMAT";
  2476.  
  2477. TITLE: %s
  2478.   ID: %s
  2479.  URL: https://www.youtube.com/playlist?list=%s
  2480. DESCR: %s
  2481. FORMAT
  2482.  
  2483.    foreach my $i (0 .. $#{$playlists}) {
  2484.        my $playlist = $playlists->[$i];
  2485.        if ($opt{results_with_details}) {
  2486.            printf(
  2487.                   "\n%s. %s\n    %s: %-25s %s: %s\n%s\n",
  2488.                   colored(sprintf('%2d', $i + 1), 'bold') => colored($yv_utils->get_title($playlist), 'bold blue'),
  2489.                   colored('Updated' => 'bold') => $yv_utils->get_publication_date($playlist),
  2490.                   colored('Author'  => 'bold') => $yv_utils->get_channel_title($playlist),
  2491.                   wrap_text(
  2492.                             i_tab => q{ } x 4,
  2493.                             s_tab => q{ } x 4,
  2494.                             text  => [$yv_utils->get_description($playlist) || 'No description available...']
  2495.                            ),
  2496.                  );
  2497.        }
  2498.        elsif ($opt{results_fixed_width}) {
  2499.  
  2500.            require List::Util;
  2501.  
  2502.            my @authors = map { $yv_utils->get_channel_title($_) } @{$playlists};
  2503.            my @dates   = map { $yv_utils->get_publication_date($_) } @{$playlists};
  2504.  
  2505.            my $author_width = List::Util::min(List::Util::max(map { length($_) } @authors), int($term_width / 5));
  2506.            my $dates_width = List::Util::max(map { length($_) } @dates);
  2507.            my $title_length = $term_width - ($author_width + $dates_width + 2 + 3 + 1 + 2);
  2508.  
  2509.            print "\n";
  2510.            foreach my $i (0 .. $#{$playlists}) {
  2511.                my $playlist = $playlists->[$i];
  2512.                printf "%s. %s %s [%*s]\n", colored(sprintf('%2d', $i + 1), 'bold'),
  2513.                  adj_width($yv_utils->get_title($playlist), $title_length),
  2514.                  adj_width($authors[$i], $author_width, 1),
  2515.                  $dates_width, $dates[$i];
  2516.            }
  2517.            last;
  2518.        }
  2519.        elsif ($opt{results_with_colors}) {
  2520.            print "\n" if $i == 0;
  2521.            printf(
  2522.                   "%s. %s (%s) [%s]\n",
  2523.                   colored(sprintf('%2d', $i + 1), 'bold'),
  2524.                   colored($yv_utils->get_title($playlist),                 'bold green'),
  2525.                   colored("by " . $yv_utils->get_channel_title($playlist), 'bold yellow'),
  2526.                   colored($yv_utils->get_publication_date($playlist),      'bold blue'),
  2527.                  );
  2528.        }
  2529.        else {
  2530.            print "\n" if $i == 0;
  2531.            printf(
  2532.                   "%s. %s (by %s) [%s]\n",
  2533.                   colored(sprintf('%2d', $i + 1), 'bold'), $yv_utils->get_title($playlist),
  2534.                   $yv_utils->get_channel_title($playlist), $yv_utils->get_publication_date($playlist)
  2535.                  );
  2536.        }
  2537.    }
  2538.  
  2539.    state @keywords;
  2540.    if ($args{auto}) { }    # do nothing...
  2541.    else {
  2542.        @keywords = get_input_for_playlists();
  2543.        if (scalar(@keywords) == 0) {
  2544.            __SUB__->(@_);
  2545.        }
  2546.    }
  2547.  
  2548.    my $contains_keywords = grep /$non_digit_or_opt_re/, @keywords;
  2549.  
  2550.    my @for_search;
  2551.    foreach my $key (@keywords) {
  2552.        if ($key =~ /$valid_opt_re/) {
  2553.  
  2554.            my $opt = $1;
  2555.  
  2556.            if (
  2557.                general_options(
  2558.                                opt  => $opt,
  2559.                                sub  => __SUB__,
  2560.                                url  => $url,
  2561.                                res  => $playlists,
  2562.                                info => $info,
  2563.                                mode => 'playlists',
  2564.                               )
  2565.              ) {
  2566.                ## ok
  2567.            }
  2568.            elsif ($opt =~ /^(?:h|help)\z/) {
  2569.                print $playlists_help;
  2570.                press_enter_to_continue();
  2571.            }
  2572.            elsif ($opt =~ /^(?:r|return)\z/) {
  2573.                return;
  2574.            }
  2575.            elsif ($opt =~ /^i(?:nfo)?${digit_or_equal_re}(.*)/) {
  2576.                if (my @ids = get_valid_numbers($#{$playlists}, $1)) {
  2577.                    foreach my $id (@ids) {
  2578.                        my $desc = wrap_text(
  2579.                                       i_tab => q{ } x 7,
  2580.                                       s_tab => q{ } x 7,
  2581.                                       text => [$yv_utils->get_description($playlists->[$id]) || 'No description available...']
  2582.                        );
  2583.                        $desc =~ s/^\s+//;
  2584.                        printf $info_format, $yv_utils->get_title($playlists->[$id]),
  2585.                          ($yv_utils->get_playlist_id($playlists->[$id])) x 2, $desc;
  2586.                    }
  2587.                    press_enter_to_continue();
  2588.                }
  2589.                else {
  2590.                    warn_no_thing_selected('playlist');
  2591.                }
  2592.            }
  2593.            elsif ($opt =~ /^pp${digit_or_equal_re}(.*)/) {
  2594.                if (my @ids = get_valid_numbers($#{$playlists}, $1)) {
  2595.                    my $arg = "--pp=" . join(q{,}, map { $yv_utils->get_playlist_id($_) } @{$playlists}[@ids]);
  2596.                    apply_input_arguments([$arg]);
  2597.                }
  2598.                else {
  2599.                    warn_no_thing_selected('playlist');
  2600.                }
  2601.            }
  2602.            else {
  2603.                warn_invalid('option', $opt);
  2604.            }
  2605.        }
  2606.        elsif (youtube_urls($key)) {
  2607.            ## ok
  2608.        }
  2609.        elsif (valid_num($key, $playlists) and not $contains_keywords) {
  2610.            if ($args{return_playlist_id}) {
  2611.                return $yv_utils->get_playlist_id($playlists->[$key - 1]);
  2612.            }
  2613.            get_and_print_videos_from_playlist($yv_utils->get_playlist_id($playlists->[$key - 1]));
  2614.        }
  2615.        else {
  2616.            push @for_search, $key;
  2617.        }
  2618.    }
  2619.  
  2620.    if (@for_search) {
  2621.        __SUB__->($yv_obj->search_playlists(\@for_search));
  2622.    }
  2623.  
  2624.    __SUB__->(@_);
  2625. }
  2626.  
  2627. sub compile_regex {
  2628.    my ($value) = @_;
  2629.    $value =~ s{^(?<quote>['"])(?<regex>.+)\g{quote}$}{$+{regex}}s;
  2630.  
  2631.    my $re = eval { use re qw(eval); qr/$value/i };
  2632.  
  2633.    if ($@) {
  2634.        warn_invalid("regex", $@);
  2635.        return;
  2636.    }
  2637.  
  2638.    return $re;
  2639. }
  2640.  
  2641. sub get_range_numbers {
  2642.    my ($first, $second) = @_;
  2643.  
  2644.    return (
  2645.            $first > $second
  2646.            ? (reverse($second .. $first))
  2647.            : ($first .. $second)
  2648.           );
  2649. }
  2650.  
  2651. sub get_valid_numbers {
  2652.    my ($max, $input) = @_;
  2653.  
  2654.    my @output;
  2655.    foreach my $id (split(/[,\s]+/, $input)) {
  2656.        push @output,
  2657.            $id =~ /$range_num_re/ ? get_range_numbers($1, $2)
  2658.          : $id =~ /^[0-9]{1,2}\z/ ? $id
  2659.          :                          next;
  2660.    }
  2661.  
  2662.    return grep { $_ >= 0 and $_ <= $max } map { $_ - 1 } @output;
  2663. }
  2664.  
  2665. sub get_streaming_url {
  2666.    my ($video_id) = @_;
  2667.  
  2668.    my ($urls, $captions, $info) = $yv_obj->get_streaming_urls($video_id);
  2669.  
  2670.    if (not defined $urls) {
  2671.        return scalar {};
  2672.    }
  2673.  
  2674.    # Download the closed-captions
  2675.    my $srt_file;
  2676.    if (    ref($captions) eq 'ARRAY'
  2677.        and @$captions
  2678.        and $opt{get_captions}
  2679.        and not $opt{novideo}) {
  2680.        require WWW::YoutubeViewer::GetCaption;
  2681.        my $yv_cap = WWW::YoutubeViewer::GetCaption->new(
  2682.                                                         auto_captions => $opt{auto_captions},
  2683.                                                         captions_dir  => $opt{captions_dir},
  2684.                                                         captions      => $captions,
  2685.                                                         languages     => $CONFIG{srt_languages},
  2686.                                                        );
  2687.        $srt_file = $yv_cap->save_caption($video_id);
  2688.    }
  2689.  
  2690.    require WWW::YoutubeViewer::Itags;
  2691.    state $yv_itags = WWW::YoutubeViewer::Itags->new();
  2692.  
  2693.    # Include DASH itags
  2694.    my $dash = 1;
  2695.  
  2696.    # Exclude DASH itags in download-mode or when no video output is required
  2697.    if ($opt{novideo} or not $opt{dash_support}) {
  2698.        $dash = 0;
  2699.    }
  2700.    elsif ($opt{download_video}) {
  2701.        $dash = $opt{merge_into_mkv} ? 1 : 0;
  2702.    }
  2703.  
  2704.    my ($streaming, $resolution) =
  2705.      $yv_itags->find_streaming_url(
  2706.                                    urls           => $urls,
  2707.                                    resolution     => $opt{resolution},
  2708.                                    dash           => $dash,
  2709.                                    dash_mp4_audio => $opt{dash_mp4_audio},
  2710.                                   );
  2711.  
  2712.    return {
  2713.            streaming  => $streaming,
  2714.            srt_file   => $srt_file,
  2715.            info       => $info,
  2716.            resolution => $resolution,
  2717.           };
  2718. }
  2719.  
  2720. sub download_from_url {
  2721.    my ($url, $output_filename) = @_;
  2722.  
  2723.    my $i = 0;
  2724.    while (-e $output_filename and not $opt{clobber} and ++$i) {
  2725.        my $last_i = $i > 1 ? $i - 1 : q{/};
  2726.        $output_filename =~ s{(?:_$last_i)?(\.\w{3,4})$}{_$i$1};
  2727.    }
  2728.  
  2729.    # Download video with wget
  2730.    if ($opt{download_with_wget}) {
  2731.        my @cmd = ($opt{wget_cmd}, ($opt{clobber} ? () : q{-nc}), $url, q{-O}, $output_filename);
  2732.  
  2733.        if ($opt{download_in_parallel}) {
  2734.            my $pid = fork() // warn "[ERROR] Can't fork: $!";
  2735.            if ($pid == 0) {
  2736.                $yv_obj->proxy_exec(@cmd, '--quiet');
  2737.            }
  2738.        }
  2739.        else {
  2740.            $yv_obj->proxy_system(@cmd);
  2741.            return if $?;
  2742.        }
  2743.    }
  2744.  
  2745.    # Download video with LWP::UserAgent
  2746.    else {
  2747.  
  2748.        require LWP::UserAgent;
  2749.  
  2750.        my $lwp = LWP::UserAgent->new(
  2751.                 show_progress => 1,
  2752.                 agent =>
  2753.                   'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36',
  2754.        );
  2755.  
  2756.        $lwp->proxy(['http', 'https'], $yv_obj->get_http_proxy)
  2757.          if defined($yv_obj->get_http_proxy);
  2758.  
  2759.        if ($opt{download_in_parallel}) {
  2760.            my $pid = fork() // warn "[ERROR] Can't fork: $!";
  2761.            if ($pid == 0) {
  2762.                $lwp->show_progress(0);
  2763.                $lwp->mirror($url, $output_filename);
  2764.                exit;
  2765.            }
  2766.        }
  2767.        else {
  2768.            my $resp = eval { $lwp->mirror($url, $output_filename) };
  2769.  
  2770.            if ($@ =~ /\bread timeout\b/i or not defined($resp) or not $resp->is_success) {
  2771.                warn colored("\n[!] Encountered an error while downloading... Trying again...", 'bold red') . "\n\n";
  2772.  
  2773.                if (-x '/usr/bin/wget') {
  2774.                    $CONFIG{download_with_wget} = 1;
  2775.                    dump_configuration();
  2776.                }
  2777.                else {
  2778.                    warn colored("[!] Please install `wget` and try again...", 'bold red') . "\n\n";
  2779.                }
  2780.  
  2781.                unlink($output_filename);
  2782.                $opt{download_with_wget} = 1;
  2783.                return download_from_url($url, $output_filename);
  2784.            }
  2785.        }
  2786.    }
  2787.  
  2788.    return $output_filename;
  2789. }
  2790.  
  2791. sub download_video {
  2792.    my ($streaming, $info) = @_;
  2793.  
  2794.    my $fat32safe = $opt{fat32safe};
  2795.    state $unix_like = $^O =~ /^(?:linux|freebsd|openbsd)\z/i;
  2796.  
  2797.    if (not $fat32safe and not $unix_like) {
  2798.        $fat32safe = 1;
  2799.    }
  2800.  
  2801.    my $video_filename = $yv_utils->format_text(
  2802.                                                streaming => $streaming,
  2803.                                                info      => $info,
  2804.                                                text      => $opt{video_filename_format},
  2805.                                                escape    => 0,
  2806.                                                fat32safe => $fat32safe,
  2807.                                               );
  2808.  
  2809.    my $audio_filename;
  2810.    my $naked_filename = $video_filename =~ s/\.\w+\z//r;
  2811.  
  2812.    my $mkv_filename = "$naked_filename.mkv";
  2813.    my $srt_filename = "$naked_filename.srt";
  2814.  
  2815.    my $video_info = $streaming->{streaming};
  2816.    my $audio_info = $streaming->{streaming}{__AUDIO__};
  2817.  
  2818.    if (not -d $opt{downloads_dir}) {
  2819.        require File::Path;
  2820.        unless (File::Path::make_path($opt{downloads_dir})) {
  2821.            warn colored("\n[!] Can't create directory '$opt{downloads_dir}': $1", 'bold red') . "\n";
  2822.        }
  2823.    }
  2824.  
  2825.    if (not -w $opt{downloads_dir}) {
  2826.        warn colored("\n[!] Can't write into directory '$opt{downloads_dir}': $!", 'bold red') . "\n";
  2827.        $opt{downloads_dir} = (-w curdir()) ? curdir() : (-w $ENV{HOME}) ? $ENV{HOME} : return;
  2828.        warn colored("[!] Video will be downloaded into directory: $opt{downloads_dir}", 'bold red') . "\n";
  2829.    }
  2830.  
  2831.    $video_filename = catfile($opt{downloads_dir}, $video_filename);
  2832.  
  2833.    if ($opt{skip_if_exists} and -e $video_filename) {
  2834.        say "[*] File `$video_filename` already exists. Skipping...";
  2835.    }
  2836.    elsif ($opt{skip_if_exists} and -e $mkv_filename) {
  2837.        $video_filename = $mkv_filename;
  2838.        say "[*] File `$mkv_filename` already exists. Skipping...";
  2839.    }
  2840.    else {
  2841.  
  2842.        # Disable `download in parallel` in combination with `download and play`
  2843.        if ($opt{download_in_parallel} and $opt{download_and_play}) {
  2844.            warn colored("[!] Downloading in parallel is not supported with `--dl-play`...", 'bold red') . "\n";
  2845.            $opt{download_in_parallel} = 0;
  2846.        }
  2847.  
  2848.        # Disable `download in parallel` in combination with `merge into mkv`
  2849.        if ($opt{download_in_parallel} and $opt{merge_into_mkv}) {
  2850.            warn colored("[!] Downloading in parallel is not supported with `--mkv-merge`...", 'bold red') . "\n";
  2851.            $opt{download_in_parallel} = 0;
  2852.        }
  2853.  
  2854.        $video_filename = download_from_url($video_info->{url}, $video_filename) // return;
  2855.        $audio_filename =
  2856.          download_from_url($audio_info->{url}, $naked_filename . ' - audio.' . $yv_utils->extension($audio_info->{type}))
  2857.          if $audio_info;
  2858.    }
  2859.  
  2860.    my @merge_files = ($video_filename);
  2861.  
  2862.    if (defined($audio_filename)) {
  2863.        push @merge_files, $audio_filename;
  2864.    }
  2865.  
  2866.    if (defined($streaming->{srt_file}) and $opt{merge_with_captions}) {
  2867.        push @merge_files, $streaming->{srt_file};
  2868.    }
  2869.  
  2870.    if ($opt{merge_into_mkv} and @merge_files > 1 and not -e $mkv_filename) {
  2871.  
  2872.        say "[*] Merging into MKV...";
  2873.        my $merge_command =
  2874.          join(' ', $opt{ffmpeg_cmd}, (map { "-i \Q$_\E" } @merge_files), $opt{merge_into_mkv_args}, "\Q$mkv_filename\E");
  2875.  
  2876.        if ($yv_obj->get_debug) {
  2877.            say "-> Command: $merge_command";
  2878.        }
  2879.  
  2880.        $yv_obj->proxy_system($merge_command);
  2881.  
  2882.        if ($? == 0 and -e $mkv_filename) {
  2883.            unlink @merge_files;
  2884.            $video_filename = $mkv_filename;
  2885.        }
  2886.    }
  2887.  
  2888.    # Convert the downloaded video
  2889.    if (defined $opt{convert_to}) {
  2890.        my $convert_filename = "$naked_filename.$opt{convert_to}";
  2891.        my $convert_cmd      = $opt{convert_cmd};
  2892.  
  2893.        my %table = (
  2894.                     'IN'  => $video_filename,
  2895.                     'OUT' => $convert_filename,
  2896.                    );
  2897.  
  2898.        my $regex = do {
  2899.            local $" = '|';
  2900.             qr/\*(@{[keys %table]})\*/;
  2901.         };
  2902.  
  2903.         $convert_cmd =~ s/$regex/\Q$table{$1}\E/g;
  2904.         say $convert_cmd if $yv_obj->get_debug;
  2905.  
  2906.         $yv_obj->proxy_system($convert_cmd);
  2907.  
  2908.         if ($? == 0) {
  2909.  
  2910.             if (not $opt{keep_original_video}) {
  2911.                 unlink $video_filename
  2912.                   or warn colored("\n[!] Can't unlink file '$video_filename': $!", 'bold red') . "\n\n";
  2913.             }
  2914.  
  2915.             $video_filename = $convert_filename if -e $convert_filename;
  2916.         }
  2917.     }
  2918.  
  2919.     # Play the download video
  2920.     if ($opt{download_and_play}) {
  2921.  
  2922.         local $streaming->{streaming}{url}       = '';
  2923.         local $streaming->{streaming}{__AUDIO__} = undef;
  2924.         local $streaming->{srt_file} = undef if ($opt{merge_into_mkv} && $opt{merge_with_captions});
  2925.  
  2926.         my $command = get_player_command($streaming, $info);
  2927.         say "-> Command: ", $command if $yv_obj->get_debug;
  2928.  
  2929.         $yv_obj->proxy_system(join(q{ }, $command, quotemeta($video_filename)));
  2930.  
  2931.         # Remove it afterwards
  2932.         if ($? == 0 and $opt{remove_played_file}) {
  2933.             unlink $video_filename
  2934.               or warn colored("\n[!] Can't unlink file '$video_filename': $!", 'bold red') . "\n\n";
  2935.         }
  2936.     }
  2937.  
  2938.     # Copy the .srt file from captions-dir to downloads-dir
  2939.     if (    $opt{copy_caption}
  2940.         and -e $video_filename
  2941.         and defined($streaming->{srt_file})
  2942.         and -e $streaming->{srt_file}) {
  2943.  
  2944.         my $from = $streaming->{srt_file};
  2945.         my $to = catfile($opt{downloads_dir}, $srt_filename);
  2946.  
  2947.         require File::Copy;
  2948.         File::Copy::cp($from, $to);
  2949.     }
  2950.  
  2951.     return 1;
  2952. }
  2953.  
  2954. sub get_player_command {
  2955.     my ($streaming, $video) = @_;
  2956.  
  2957.     $MPLAYER{fullscreen} = $opt{fullscreen} ? $opt{video_players}{$opt{video_player_selected}}{fs} // ''      : q{};
  2958.     $MPLAYER{novideo}    = $opt{novideo}    ? $opt{video_players}{$opt{video_player_selected}}{novideo} // '' : q{};
  2959.     $MPLAYER{mplayer_arguments} = $opt{video_players}{$opt{video_player_selected}}{arg} // q{};
  2960.  
  2961.     my $cmd = join(
  2962.         q{ },
  2963.         (
  2964.             # Video player
  2965.             $opt{video_players}{$opt{video_player_selected}}{cmd},
  2966.  
  2967.             (    # Audio file (https://)
  2968.                ref($streaming->{streaming}{__AUDIO__}) eq 'HASH'
  2969.                  && exists($opt{video_players}{$opt{video_player_selected}}{audio})
  2970.                ? $opt{video_players}{$opt{video_player_selected}}{audio}
  2971.                : ()
  2972.             ),
  2973.  
  2974.             (    # Subtitle file (.srt)
  2975.                defined($streaming->{srt_file})
  2976.                  && exists($opt{video_players}{$opt{video_player_selected}}{srt})
  2977.                ? $opt{video_players}{$opt{video_player_selected}}{srt}
  2978.                : ()
  2979.             ),
  2980.  
  2981.             # Rest of the arguments
  2982.             grep({ defined($_) and /\S/ } values %MPLAYER)
  2983.         )
  2984.     );
  2985.  
  2986.     my $has_video = $cmd =~ /\*(?:VIDEO|URL|ID)\*/;
  2987.  
  2988.     $cmd = $yv_utils->format_text(
  2989.                                   streaming => $streaming,
  2990.                                   info      => $video,
  2991.                                   text      => $cmd,
  2992.                                   escape    => 1,
  2993.                                  );
  2994.  
  2995.     $has_video ? $cmd : join(' ', $cmd, quotemeta($streaming->{streaming}{url}));
  2996. }
  2997.  
  2998. sub play_videos {
  2999.     my ($videos) = @_;
  3000.  
  3001.     my @streaming_urls;
  3002.     foreach my $video (@{$videos}) {
  3003.  
  3004.         my $video_id = $yv_utils->get_video_id($video);
  3005.  
  3006.         # It may be downloaded, but that's OK...
  3007.         if ($opt{highlight_watched}) {
  3008.             $watched_videos{$video_id} = 1;
  3009.         }
  3010.  
  3011.         if (defined($opt{max_seconds}) and $opt{max_seconds} >= 0) {
  3012.             next if $yv_utils->get_duration($video) > $opt{max_seconds};
  3013.         }
  3014.  
  3015.         if (defined($opt{min_seconds}) and $opt{min_seconds} >= 0) {
  3016.             next if $yv_utils->get_duration($video) < $opt{min_seconds};
  3017.         }
  3018.  
  3019.         my $streaming = get_streaming_url($video_id);
  3020.  
  3021.         if (ref($streaming->{streaming}) ne 'HASH') {
  3022.             warn colored("[!] No streaming URL has been found...", 'bold red') . "\n";
  3023.             next;
  3024.         }
  3025.  
  3026.         if (   !defined($streaming->{streaming}{url})
  3027.             and defined($streaming->{info}{status})
  3028.             and $streaming->{info}{status} =~ /(?:error|fail)/i) {
  3029.             warn colored("[!] Error on: ", 'bold red') . sprintf($CONFIG{youtube_video_url}, $video_id) . "\n";
  3030.             warn colored("[*] Reason: ", 'bold red') . $streaming->{info}{reason} =~ tr/+/ /r . "\n\n";
  3031.         }
  3032.  
  3033.         # Dump metadata information
  3034.         if (defined($opt{dump})) {
  3035.  
  3036.             my $file = $video_id . '.' . $opt{dump};
  3037.             open(my $fh, '>:utf8', $file)
  3038.               or die "Can't open file `$file' for writing: $!";
  3039.  
  3040.             local $video->{streaming} = $streaming;
  3041.  
  3042.             if ($opt{dump} eq 'json') {
  3043.                 print {$fh} JSON->new->pretty(1)->encode($video);
  3044.             }
  3045.             elsif ($opt{dump} eq 'perl') {
  3046.                 require Data::Dump;
  3047.                 print {$fh} Data::Dump::pp($video);
  3048.             }
  3049.  
  3050.             close $fh;
  3051.         }
  3052.  
  3053.         if ($opt{download_video}) {
  3054.             print_video_info($video);
  3055.             if (not download_video($streaming, $video)) {
  3056.                 return;
  3057.             }
  3058.         }
  3059.         elsif (length($opt{extract_info})) {
  3060.             my $fh = $opt{extract_info_fh} // \*STDOUT;
  3061.             say {$fh}
  3062.               $yv_utils->format_text(
  3063.                                      streaming => $streaming,
  3064.                                      info      => $video,
  3065.                                      text      => $opt{extract_info},
  3066.                                      escape    => $opt{escape_info},
  3067.                                      fat32safe => $opt{fat32safe},
  3068.                                     );
  3069.         }
  3070.         elsif ($opt{combine_multiple_videos}) {
  3071.             print_video_info($video);
  3072.             push @streaming_urls, $streaming;
  3073.         }
  3074.         else {
  3075.             print_video_info($video);
  3076.             my $command = get_player_command($streaming, $video);
  3077.  
  3078.             if ($yv_obj->get_debug) {
  3079.                 say "-> Resolution: $streaming->{resolution}";
  3080.                 say "-> Video itag: $streaming->{streaming}{itag}";
  3081.                 say "-> Audio itag: $streaming->{streaming}{__AUDIO__}{itag}" if exists $streaming->{streaming}{__AUDIO__};
  3082.                 say "-> Video type: $streaming->{streaming}{type}";
  3083.                 say "-> Audio type: $streaming->{streaming}{__AUDIO__}{type}" if exists $streaming->{streaming}{__AUDIO__};
  3084.                 say "-> Command: $command";
  3085.             }
  3086.  
  3087.             $yv_obj->proxy_system($command);    # execute the video player
  3088.             if ($? and $? != 512) {
  3089.                 $opt{auto_next_page} = 0;
  3090.                 return;
  3091.             }
  3092.         }
  3093.  
  3094.         press_enter_to_continue() if $opt{confirm};
  3095.     }
  3096.  
  3097.     if ($opt{combine_multiple_videos} && @streaming_urls) {
  3098.         my $streaming = $streaming_urls[0];
  3099.  
  3100.         my $command = get_player_command($streaming, $videos->[0]);
  3101.         say $command if $yv_obj->get_debug;
  3102.  
  3103.         $yv_obj->proxy_system(join(q{ }, $command, map { quotemeta($_->{streaming}{url}) } @streaming_urls));
  3104.         return if $?;
  3105.     }
  3106.  
  3107.     return 1;
  3108. }
  3109.  
  3110. sub play_videos_matched_by_regex {
  3111.     my %args = @_;
  3112.  
  3113.     my $key    = $args{key};
  3114.     my $regex  = $args{regex};
  3115.     my $videos = $args{videos};
  3116.  
  3117.     my $sub = \&{'WWW::YoutubeViewer::Utils' . '::' . 'get_' . $key};
  3118.  
  3119.     if (not defined &$sub) {
  3120.         warn colored("\n[!] Invalid key: <$key>.", 'bold red') . "\n";
  3121.         return;
  3122.     }
  3123.  
  3124.     if (defined(my $re = compile_regex($regex))) {
  3125.         if (my @nums = grep { $yv_utils->$sub($videos->[$_]) =~ /$re/ } 0 .. $#{$videos}) {
  3126.             if (not play_videos([@{$videos}[@nums]])) {
  3127.                 return;
  3128.             }
  3129.         }
  3130.         else {
  3131.             warn colored("\n[!] No video <$key> matched by the regex: $re", 'bold red') . "\n";
  3132.             return;
  3133.         }
  3134.     }
  3135.  
  3136.     return 1;
  3137. }
  3138.  
  3139. sub print_video_info {
  3140.     my ($video) = @_;
  3141.  
  3142.     my $hr = '-' x ($opt{get_term_width} ? get_term_width() : $term_width);
  3143.  
  3144.     printf(
  3145.            "\n%s %s\n%s\n%s\n%s\n%s",
  3146.            _bold_color('=>>'),
  3147.            'Description',
  3148.            $hr,
  3149.            wrap_text(
  3150.                      i_tab => q{},
  3151.                      s_tab => q{},
  3152.                      text  => [$yv_utils->get_description($video) || 'No description available...']
  3153.                     ),
  3154.            $hr,
  3155.            _bold_color('* URL: ')
  3156.           );
  3157.  
  3158.     print STDOUT sprintf($CONFIG{youtube_video_url}, $yv_utils->get_video_id($video));
  3159.  
  3160.     my $title        = $yv_utils->get_title($video);
  3161.     my $title_length = length($title);
  3162.     my $rep          = ($term_width - $title_length) / 2 - 4;
  3163.  
  3164.     $rep = 0 if $rep < 0;
  3165.  
  3166.     print "\n$hr\n", q{ } x $rep => (_bold_color("=>> $title <<=") . "\n\n"),
  3167.       map(sprintf(q{-> } . "%-*s: %s\n", $opt{_colors} ? 18 : 10, _bold_color($_->[0]), $_->[1]),
  3168.           (
  3169.            ['Channel'    => $yv_utils->get_channel_title($video)],
  3170.            ['ChannelID'  => $yv_utils->get_channel_id($video)],
  3171.            ['Category'   => $yv_utils->get_category_name($video)],
  3172.            ['Definition' => $yv_utils->get_definition($video)],
  3173.            ['Duration'   => $yv_utils->format_time($yv_utils->get_duration($video))],
  3174.            ['Likes'      => $yv_utils->set_thousands($yv_utils->get_likes($video))],
  3175.            ['Dislikes'   => $yv_utils->set_thousands($yv_utils->get_dislikes($video))],
  3176.            ['Comments'   => $yv_utils->set_thousands($yv_utils->get_comments($video))],
  3177.            ['Views'      => $yv_utils->set_thousands($yv_utils->get_views($video))],
  3178.            ['Published'  => $yv_utils->get_publication_date($video)],
  3179.             )),
  3180.       "$hr\n";
  3181.  
  3182.     return 1;
  3183. }
  3184.  
  3185. sub print_videos {
  3186.     my ($results, %args) = @_;
  3187.  
  3188.     if (not $yv_utils->has_entries($results)) {
  3189.         warn_no_results("video");
  3190.     }
  3191.  
  3192.     if ($opt{get_term_width} and $opt{results_fixed_width}) {
  3193.         get_term_width();
  3194.     }
  3195.  
  3196.     my $url    = $results->{url};
  3197.     my $info   = $results->{results} // {};
  3198.     my $videos = $info->{items} // [];
  3199.  
  3200. #<<<
  3201.     @$videos = grep {
  3202.             ref($_) eq 'HASH' && ref($_->{id}) eq 'HASH'
  3203.                 ? (exists($_->{id}{kind})
  3204.                     ? $_->{id}{kind} eq 'youtube#video'
  3205.                     : 0)
  3206.                 : 1
  3207.     } @$videos;
  3208. #>>>
  3209.  
  3210.     if ($opt{shuffle}) {
  3211.         require List::Util;
  3212.         $videos = [List::Util::shuffle(@{$videos})];
  3213.     }
  3214.  
  3215.     if (@{$videos} and not $results->{has_extra_info}) {
  3216.         my $content_details = $yv_obj->video_details(join(',', map { $yv_utils->get_video_id($_) } @{$videos}), VIDEO_PART);
  3217.         my $video_details = $content_details->{results}{items};
  3218.         foreach my $i (0 .. $#{$videos}) {
  3219.             @{$videos->[$i]}{qw(id contentDetails statistics snippet)} =
  3220.               @{$video_details->[$i]}{qw(id contentDetails statistics snippet)};
  3221.         }
  3222.         $results->{has_extra_info} = 1;
  3223.     }
  3224.  
  3225. #<<<
  3226.     # Filter out private or deleted videos
  3227.     @$videos = grep {
  3228.         defined($yv_utils->get_video_id($_))
  3229.     } @$videos;
  3230. #>>>
  3231.  
  3232.     my @formatted;
  3233.  
  3234.     foreach my $i (0 .. $#{$videos}) {
  3235.         my $video = $videos->[$i];
  3236.  
  3237.         if ($opt{custom_layout}) {
  3238.  
  3239.             my $entry = $opt{custom_layout_format};
  3240.             $entry =~ s/\*NO\*/sprintf('%2d', $i+1)/ge;
  3241.  
  3242.             $entry = $yv_utils->format_text(
  3243.                                             info   => $video,
  3244.                                             text   => $entry,
  3245.                                             escape => 0,
  3246.                                            );
  3247.  
  3248.             push @formatted, $entry;
  3249.         }
  3250.         elsif ($opt{results_with_details}) {
  3251.             push @formatted,
  3252.               ($i == 0 ? '' : "\n")
  3253.               . sprintf(
  3254.                         "%s. %s\n" . "    %s: %-16s %s: %-13s %s: %s\n" . "    %s: %-12s %s: %-10s %s: %s\n%s\n",
  3255.                         colored(sprintf('%2d', $i + 1), 'bold') => colored($yv_utils->get_title($video), 'bold blue'),
  3256.                         colored('Views'     => 'bold') => $yv_utils->set_thousands($yv_utils->get_views($video)),
  3257.                         colored('Likes'     => 'bold') => $yv_utils->set_thousands($yv_utils->get_likes($video)),
  3258.                         colored('Dislikes'  => 'bold') => $yv_utils->set_thousands($yv_utils->get_dislikes($video)),
  3259.                         colored('Published' => 'bold') => $yv_utils->get_publication_date($video),
  3260.                         colored('Duration'  => 'bold') => $yv_utils->format_time($yv_utils->get_duration($video)),
  3261.                         colored('Author'    => 'bold') => $yv_utils->get_channel_title($video),
  3262.                         wrap_text(
  3263.                                   i_tab => q{ } x 4,
  3264.                                   s_tab => q{ } x 4,
  3265.                                   text  => [$yv_utils->get_description($video) || 'No description available...']
  3266.                                  ),
  3267.                        );
  3268.         }
  3269.         elsif ($opt{results_with_colors}) {
  3270.             my $definition = $yv_utils->get_definition($video);
  3271.             push @formatted,
  3272.               sprintf(
  3273.                       "%s. %s (%s) [%s]\n",
  3274.                       colored(sprintf('%2d', $i + 1), 'bold'),
  3275.                       colored($yv_utils->get_title($video),                            'bold green'),
  3276.                       colored("by " . $yv_utils->get_channel_title($video),            'bold yellow'),
  3277.                       colored($yv_utils->format_time($yv_utils->get_duration($video)), 'bold bright_blue'),
  3278.                      );
  3279.         }
  3280.         elsif ($opt{results_fixed_width}) {
  3281.  
  3282.             require List::Util;
  3283.  
  3284.             my @durations = map { $yv_utils->get_duration($_) } @{$videos};
  3285.             my @authors   = map { $yv_utils->get_channel_title($_) } @{$videos};
  3286.  
  3287.             my $author_width = List::Util::min(List::Util::max(map { length($_) } @authors) || 1, int($term_width / 5));
  3288.             my $time_width = List::Util::first(sub { $_ >= 3600 }, @durations) ? 8 : 6;
  3289.             my $title_length = $term_width - ($author_width + $time_width + 3 + 2 + 1);
  3290.  
  3291.             foreach my $i (0 .. $#{$videos}) {
  3292.                 my $video = $videos->[$i];
  3293.                 push @formatted,
  3294.                   sprintf("%s. %s %s %*s\n",
  3295.                           colored(sprintf('%2d', $i + 1), 'bold'),
  3296.                           adj_width($yv_utils->get_title($video), $title_length),
  3297.                           adj_width($yv_utils->get_channel_title($video), $author_width, 1),
  3298.                           $time_width,
  3299.                           $yv_utils->format_time($durations[$i]));
  3300.             }
  3301.             last;
  3302.         }
  3303.         else {
  3304.             push @formatted,
  3305.               sprintf(
  3306.                       "%s. %s (by %s) [%s]\n",
  3307.                       colored(sprintf('%2d', $i + 1), 'bold'), $yv_utils->get_title($video),
  3308.                       $yv_utils->get_channel_title($video), $yv_utils->format_time($yv_utils->get_duration($video)),
  3309.                      );
  3310.         }
  3311.     }
  3312.  
  3313.     if ($opt{highlight_watched}) {
  3314.         foreach my $i (0 .. $#{$videos}) {
  3315.             my $video = $videos->[$i];
  3316.             if (exists($watched_videos{$yv_utils->get_video_id($video)})) {
  3317.                 $formatted[$i] = colored(colorstrip($formatted[$i]), $opt{highlight_color});
  3318.             }
  3319.         }
  3320.     }
  3321.  
  3322.     if (@formatted) {
  3323.         print "\n" . join("", @formatted);
  3324.     }
  3325.  
  3326.     if ($opt{play_all} || $opt{play_backwards}) {
  3327.         if (@{$videos}) {
  3328.             if (
  3329.                 play_videos(
  3330.                             $opt{play_backwards}
  3331.                             ? [reverse @{$videos}]
  3332.                             : $videos
  3333.                            )
  3334.               ) {
  3335.                 if ($opt{play_backwards}) {
  3336.                     if (defined $info->{prevPageToken}) {
  3337.                         __SUB__->($yv_obj->previous_page($url, $info->{prevPageToken}), auto => 1);
  3338.                     }
  3339.                     else {
  3340.                         $opt{play_backwards} = 0;
  3341.                         warn_first_page();
  3342.                         return;
  3343.                     }
  3344.                 }
  3345.                 else {
  3346.                     if (defined $info->{nextPageToken}) {
  3347.                         __SUB__->($yv_obj->next_page($url, $info->{nextPageToken}), auto => 1);
  3348.                     }
  3349.                     else {
  3350.                         $opt{play_all} = 0;
  3351.                         warn_last_page();
  3352.                         return;
  3353.                     }
  3354.                 }
  3355.             }
  3356.             else {
  3357.                 $opt{play_all}       = 0;
  3358.                 $opt{play_backwards} = 0;
  3359.                 __SUB__->($results);
  3360.             }
  3361.         }
  3362.         else {
  3363.             $opt{play_all}       = 0;
  3364.             $opt{play_backwards} = 0;
  3365.         }
  3366.     }
  3367.  
  3368.     state @keywords;
  3369.     if ($args{auto}) { }    # do nothing...
  3370.     else {
  3371.         @keywords = get_input_for_search();
  3372.  
  3373.         if (scalar(@keywords) == 0) {    # only arguments
  3374.             __SUB__->($results);
  3375.         }
  3376.     }
  3377.  
  3378.     state @for_search;
  3379.     state @for_play;
  3380.  
  3381.     my @copy_of_keywords = @keywords;
  3382.     my $contains_keywords = grep /$non_digit_or_opt_re/, @keywords;
  3383.  
  3384.     while (@keywords) {
  3385.         my $key = shift @keywords;
  3386.         if ($key =~ /$valid_opt_re/) {
  3387.  
  3388.             my $opt = $1;
  3389.  
  3390.             if (
  3391.                 general_options(opt => $opt,
  3392.                                 res => $videos,)
  3393.               ) {
  3394.                 ## ok
  3395.             }
  3396.             elsif ($opt =~ /^(?:h|help)\z/) {
  3397.                 print $complete_help;
  3398.                 press_enter_to_continue();
  3399.             }
  3400.             elsif ($opt =~ /^(?:n|next)\z/) {
  3401.                 if (defined $info->{nextPageToken}) {
  3402.                     my $request = $yv_obj->next_page($url, $info->{nextPageToken});
  3403.                     __SUB__->($request, @keywords ? (auto => 1) : ());
  3404.                 }
  3405.                 else {
  3406.                     warn_last_page();
  3407.                     if ($opt{auto_next_page}) {
  3408.                         $opt{auto_next_page} = 0;
  3409.                         @copy_of_keywords = ();
  3410.                         last;
  3411.                     }
  3412.                 }
  3413.             }
  3414.             elsif ($opt =~ /^(?:b|back|p|prev|previous)\z/) {
  3415.                 if (defined $info->{prevPageToken}) {
  3416.                     __SUB__->($yv_obj->previous_page($url, $info->{prevPageToken}), @keywords ? (auto => 1) : ());
  3417.                 }
  3418.                 else {
  3419.                     warn_first_page();
  3420.                 }
  3421.             }
  3422.             elsif ($opt =~ /^(?:R|refresh)\z/) {
  3423.                 @{$videos} = @{$yv_obj->_get_results($url)->{results}{items}};
  3424.             }
  3425.             elsif ($opt =~ /^(?:r|return)\z/) {
  3426.                 return;
  3427.             }
  3428.             elsif ($opt =~ /^(?:a(?:uthor)?|u)${digit_or_equal_re}(.*)/) {
  3429.                 if (my @nums = get_valid_numbers($#{$videos}, $1)) {
  3430.                     foreach my $id (@nums) {
  3431.                         my $channel_id = $yv_utils->get_channel_id($videos->[$id]);
  3432.                         my $request    = $yv_obj->uploads($channel_id);
  3433.                         if ($yv_utils->has_entries($request)) {
  3434.                             __SUB__->($request);
  3435.                         }
  3436.                         else {
  3437.                             warn_no_results('video');
  3438.                         }
  3439.                     }
  3440.                 }
  3441.                 else {
  3442.                     warn_no_thing_selected('video');
  3443.                 }
  3444.             }
  3445.             elsif ($opt =~ /^(?:ps|s2p)${digit_or_equal_re}(.*)/) {
  3446.                 if (my @nums = get_valid_numbers($#{$videos}, $1)) {
  3447.                     select_and_save_to_playlist(map { $yv_utils->get_video_id($videos->[$_]) } @nums);
  3448.                 }
  3449.                 else {
  3450.                     warn_no_thing_selected('video');
  3451.                 }
  3452.             }
  3453.             elsif ($opt =~ /^(?:p(?:laylists?)?|up)${digit_or_equal_re}(.*)/) {
  3454.                 if (my @nums = get_valid_numbers($#{$videos}, $1)) {
  3455.                     foreach my $id (@nums) {
  3456.                         my $request = $yv_obj->playlists($yv_utils->get_channel_id($videos->[$id]));
  3457.                         if ($yv_utils->has_entries($request)) {
  3458.                             print_playlists($request);
  3459.                         }
  3460.                         else {
  3461.                             warn_no_results('playlist');
  3462.                         }
  3463.                     }
  3464.                 }
  3465.                 else {
  3466.                     warn_no_thing_selected('video');
  3467.                 }
  3468.             }
  3469.             elsif ($opt =~ /^((?:dis)?like)${digit_or_equal_re}(.*)/) {
  3470.                 my $rating = $1;
  3471.                 if (my @nums = get_valid_numbers($#{$videos}, $2)) {
  3472.                     rate_videos($rating, map { $yv_utils->get_video_id($videos->[$_]) } @nums);
  3473.                 }
  3474.                 else {
  3475.                     warn_no_thing_selected('video');
  3476.                 }
  3477.             }
  3478.             elsif ($opt =~ /^(?:fav(?:orite)?|F)${digit_or_equal_re}(.*)/) {
  3479.                 if (my @nums = get_valid_numbers($#{$videos}, $1)) {
  3480.                     favorite_videos(map { $yv_utils->get_video_id($videos->[$_]) } @nums);
  3481.                 }
  3482.                 else {
  3483.                     warn_no_thing_selected('video');
  3484.                 }
  3485.             }
  3486.             elsif ($opt =~ /^(?:subscribe|S)${digit_or_equal_re}(.*)/) {
  3487.                 if (my @nums = get_valid_numbers($#{$videos}, $1)) {
  3488.                     subscribe_to_channels(map { $yv_utils->get_channel_id($videos->[$_]) } @nums);
  3489.                 }
  3490.                 else {
  3491.                     warn_no_thing_selected('video');
  3492.                 }
  3493.             }
  3494.             elsif ($opt =~ /^(?:en)?q(?:ueue)?+${digit_or_equal_re}(.*)/) {
  3495.                 if (my @nums = get_valid_numbers($#{$videos}, $1)) {
  3496.                     push @{$opt{_queue_play}}, map { $yv_utils->get_video_id($videos->[$_]) } @nums;
  3497.                 }
  3498.                 else {
  3499.                     warn_no_thing_selected('video');
  3500.                 }
  3501.             }
  3502.             elsif ($opt =~ /^(?:pq|qp|play-queue)\z/) {
  3503.                 if (ref $opt{_queue_play} eq 'ARRAY' and @{$opt{_queue_play}}) {
  3504.                     my $ids = 'v=' . join(q{,}, splice @{$opt{_queue_play}});
  3505.                     general_options(opt => $ids);
  3506.                 }
  3507.                 else {
  3508.                     warn colored("\n[!] The playlist is empty!", 'bold red') . "\n";
  3509.                 }
  3510.             }
  3511.             elsif ($opt =~ /^c(?:omments?)?${digit_or_equal_re}(.*)/) {
  3512.                 if (my @nums = get_valid_numbers($#{$videos}, $1)) {
  3513.                     get_and_print_comments(map { $yv_utils->get_video_id($videos->[$_]) } @nums);
  3514.                 }
  3515.                 else {
  3516.                     warn_no_thing_selected('video');
  3517.                 }
  3518.             }
  3519.             elsif ($opt =~ /^r(?:elated)?${digit_or_equal_re}(.*)/) {
  3520.                 if (my ($id) = get_valid_numbers($#{$videos}, $1)) {
  3521.                     get_and_print_related_videos($yv_utils->get_video_id($videos->[$id]));
  3522.                 }
  3523.                 else {
  3524.                     warn_no_thing_selected('video');
  3525.                 }
  3526.             }
  3527.             elsif ($opt =~ /^d(?:ownload)?${digit_or_equal_re}(.*)/) {
  3528.                 if (my @nums = get_valid_numbers($#{$videos}, $1)) {
  3529.                     local $opt{download_video} = 1;
  3530.                     play_videos([@{$videos}[@nums]]);
  3531.                 }
  3532.                 else {
  3533.                     warn_no_thing_selected('video');
  3534.                 }
  3535.             }
  3536.             elsif ($opt =~ /^(?:play|P)${digit_or_equal_re}(.*)/) {
  3537.                 if (my @nums = get_valid_numbers($#{$videos}, $1)) {
  3538.                     local $opt{download_video} = 0;
  3539.                     local $opt{extract_info}   = undef;
  3540.                     play_videos([@{$videos}[@nums]]);
  3541.                 }
  3542.                 else {
  3543.                     warn_no_thing_selected('video');
  3544.                 }
  3545.             }
  3546.             elsif ($opt =~ /^i(?:nfo)?${digit_or_equal_re}(.*)/) {
  3547.                 if (my @nums = get_valid_numbers($#{$videos}, $1)) {
  3548.                     foreach my $num (@nums) {
  3549.                         print_video_info($videos->[$num]);
  3550.                     }
  3551.                     press_enter_to_continue();
  3552.                 }
  3553.                 else {
  3554.                     warn_no_thing_selected('video');
  3555.                 }
  3556.             }
  3557.             elsif ($opt eq 'anp') {    # auto-next-page
  3558.                 $opt{auto_next_page} = 1;
  3559.             }
  3560.             elsif ($opt eq 'nnp') {    # no-next-page
  3561.                 $opt{auto_next_page} = 0;
  3562.             }
  3563.             elsif ($opt =~ /^[ks]re(?:gex)?=(.*)/) {
  3564.                 my $value = $1;
  3565.                 if ($value =~ /^([a-zA-Z]++)(?>,|=>)(.+)/) {
  3566.                     play_videos_matched_by_regex(
  3567.                                                  key    => $1,
  3568.                                                  regex  => $2,
  3569.                                                  videos => $videos,
  3570.                                                 )
  3571.                       or __SUB__->($results);
  3572.                 }
  3573.                 else {
  3574.                     warn_invalid("Special Regexp", $value);
  3575.                 }
  3576.             }
  3577.             elsif ($opt =~ /^re(?:gex)?=(.*)/) {
  3578.                 play_videos_matched_by_regex(
  3579.                                              key    => 'title',
  3580.                                              regex  => $1,
  3581.                                              videos => $videos,
  3582.                                             )
  3583.                   or __SUB__->($results);
  3584.             }
  3585.             else {
  3586.                 warn_invalid('option', $opt);
  3587.             }
  3588.         }
  3589.         elsif (youtube_urls($key)) {
  3590.             ## ok
  3591.         }
  3592.         elsif (!$contains_keywords and (valid_num($key, $videos) or $key =~ /$range_num_re/)) {
  3593.             my @for_play;
  3594.             if ($key =~ /$range_num_re/) {
  3595.                 my $from = $1;
  3596.                 my $to   = $2 // do {
  3597.                     $opt{auto_next_page} ? do { $from = 1 } : do { $opt{auto_next_page} = 1 };
  3598.                     $#{$videos} + 1;
  3599.                 };
  3600.                 my @ids = get_valid_numbers($#{$videos}, "$from..$to");
  3601.                 continue if not @ids;
  3602.                 push @for_play, @ids;
  3603.             }
  3604.             else {
  3605.                 push @for_play, $key - 1;
  3606.             }
  3607.             if (not play_videos([@{$videos}[@for_play]])) {
  3608.                 __SUB__->($results);
  3609.             }
  3610.             if ($opt{autohide_watched}) {
  3611.                 splice(@{$videos}, $key, 1) for @for_play;
  3612.             }
  3613.         }
  3614.         else {
  3615.             push @for_search, $key;
  3616.         }
  3617.     }
  3618.  
  3619.     if (@for_search) {
  3620.         __SUB__->($yv_obj->search_videos([splice(@for_search)]));
  3621.     }
  3622.     elsif ($opt{auto_next_page}) {
  3623.         @keywords = (':next', grep { $_ !~ /^:(n|next|anp)\z/ } @copy_of_keywords);
  3624.  
  3625.         if (@keywords > 1) {
  3626.             my $timeout = 2;
  3627.             print colored("\n[*] Press <ENTER> in $timeout seconds to stop the :anp option.", 'bold green');
  3628.             eval {
  3629.                 local $SIG{ALRM} = sub {
  3630.                     die "alarm\n";
  3631.                 };
  3632.                 alarm $timeout;
  3633.                 scalar <STDIN>;
  3634.                 alarm 0;
  3635.             };
  3636.  
  3637.             if ($@) {
  3638.                 if ($@ eq "alarm\n") {
  3639.                     __SUB__->($results, auto => 1);
  3640.                 }
  3641.                 else {
  3642.                     warn colored("\n[!] Unexpected error: <$@>.", 'bold red') . "\n";
  3643.                 }
  3644.             }
  3645.             else {
  3646.                 $opt{auto_next_page} = 0;
  3647.                 __SUB__->($results);
  3648.             }
  3649.         }
  3650.         else {
  3651.             warn colored("\n[!] Option ':anp' works only combined with other options!", 'bold red') . "\n";
  3652.             $opt{auto_next_page} = 0;
  3653.             __SUB__->($results);
  3654.         }
  3655.     }
  3656.  
  3657.     __SUB__->($results) if not $args{auto};
  3658.  
  3659.     return 1;
  3660. }
  3661.  
  3662. sub press_enter_to_continue {
  3663.     scalar $term->readline(colored("\n=>> Press ENTER to continue...", 'bold'));
  3664. }
  3665.  
  3666. sub main_quit {
  3667.     exit($_[0] // 0);
  3668. }
  3669.  
  3670. main_quit(0);
Add Comment
Please, Sign In to add comment