#!/usr/bin/perl 
#
# Copyright (C) 2010-2017 Trizen <echo dHJpemVueEBnbWFpbC5jb20K | base64 -d>.
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of either: the GNU General Public License as published
# by the Free Software Foundation; or the Artistic License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
# See https://dev.perl.org/licenses/ for more information.
#
#-------------------------------------------------------
#  youtube-viewer
#  Created on: 02 June 2010
#  Latest edit on: 19 April 2017
#  https://github.com/trizen/youtube-viewer
#-------------------------------------------------------
#
# youtube-viewer is a command line utility for streaming YouTube videos in mpv/vlc/mplayer.
#
# [CHANGELOG]
# - Added the `--fat32safe` option to make filenames FAT32-safe, by replacing bad characters.  - NEW (v3.2.5)
# - Added the `--highlight` option to remember and highlight watched videos in a session.      - NEW (v3.2.2)
# - Added the `--channel-videos=s` command-line option to display videos from a channel ID.    - NEW (v3.2.1)
# - Added the `i..` range, which plays all the videos, starting with video `i`. (#114)         - NEW (v3.2.1)
# - Added support for downloading multiple videos in parallel (--dl-parallel)                  - NEW (v3.1.4)
# - Migration to APIv3; some features has been lost in the process, but nothing too critical.  - NEW (v3.1.4)
# - Added built-in support for [auto-generated] closed-captions (gcap is no longer required)   - NEW (v3.1.3)
# - Added support for specifying the filename format when downloading videos (--filename=s)    - NEW (v3.1.3)
# - Added support for special tokens and for extracting information about videos (--extract=s) - NEW (v3.1.3)
# - Added support for playing DASH videos and the `--no-dash` option to deactivate it at will  - NEW (v3.1.2)
# - Added the options `--ps` and `--pid` to add videos by URL or ID to a given playlistID      - NEW (v3.1.2)
# - Added the stdin commands ":ps" and ":s2p" to add one or more videos to a selected playlist - NEW (v3.1.2)
# - Added the `--std-input=s` option which can be used to specify the first standard input     - NEW (v3.1.1)
# - Added input history support across sessions, with the 'history' config-key                 - NEW (v3.1.1)
# - Added support for more video players (--player=s) // Added support for shows (--us=author) - NEW (v3.1.0)
# - Added support to --copy-caption for downloaded videos // Added fixed-width for playlists   - NEW (v3.1.0)
# - Added the ':play' stdin option // Added the support for 'watch_later' videos (-L)          - NEW (v3.1.0)
# - Added a new option to combine multiple videos into one play instance (--combine-multiple)  - NEW (v3.1.0)
# - New authentication support (OAuth 2.0) // Added support for download & play (-dp)          - (v3.0.7)
# - Added support for YouTube EDU categories (-edu) // Options: :dv, :lec, :courses, :course   - (v3.0.4)
# - Some minor bug-fixes // New options has been added: --pp, :pp, :anp, :kregex               - (v3.0.3)
# - Added support for more resolutions, UTF-8 support, --convert-to=FMT and many bug-fixes     - (v3.0.1)
# - Youtube Viewer 3.0 has been released! New options, better functionality and new bugs :)    - (v3.0.0)
# - Added support for detailed results (usage: -D or --details) // Support for comments        - (v2.5.8)
# - Switched to Term::ReadLine for a better STDIN support // Better colors // Info support     - (v2.5.7)
# - Added support for: -duration, -caption=s, -safe-search=s, -hd // Improved code quality     - (v2.5.6)
# - Added support for configuration file, improved stability, improved debug mode              - (v2.5.5)
# - Switched to XML::Fast for parsing gdata XML, in consequence, youtube-viewer is faster!     - (v2.5.5)
# - Switched to Getopt::Long, added SIGINT handler and a better way to execute mplayer         - (v2.5.5)
# - Added support to list playlists created by a specific user (usage: -up <USERNAME>)         - (v2.5.4)
# - Improved parsing support for arguments, including arguments specified via STDIN.           - (v2.5.4)
# - Added support to search for videos uploaded by a particular YouTube user (-author=USER)    - (v2.5.4)
# - Added support to get video results starting with a predefined page (e.g.: -page=4)         - (v2.5.4)
# - Added support for previous page and support to list youtube usernames from a file          - (v2.5.2)
# - Added colors for text (--use_colors), 360p support (-3), playlist support                  - (v2.5.0)
# - Added support for today and all time Youtube tops (usage: -t, --tops, -a, --all-time)      - (v2.4.*)
# - Re-added the support for the next page / Added support for download (-d, --download)       - (v2.4.*)
# - Added support for Youtube CCaptions. (Depends on: 'gcap' - http://gcap.googlecode.com)     - (v2.4.*)
# - First version with Windows support. Require SMPlayer to play videos. See MPlayer Line      - (v2.4.*)
# - Code has been changed in a proportion of ~60% and optimized for speed // --480 became -4   - (v2.4.*)
# - Added mega-powers of omnibox to the STDIN :)                                               - (v2.3.*)
# - Re-added the option to list and play youtube videos from a user profile. Usage: -u [user]  - (v2.3.*)
# - Added a new option to play only the audio track of a videoclip. Usage: [words] -n          - (v2.3.*)
# - Added option for fullscreen (-f, --fullscreen). Usage: youtube-viewer [words] -f           - (v2.3.*)
# - Added one new option '-c'. It shows available categories and will let you to choose one.   - (v2.3.*)
# - Added one new option '-m'. It shows 3 pages of youtube video results. Usage: [words] -m    - (v2.3.*)
# - For "-A" option has been added 3 pages of youtube video results (50 clips)                 - (v2.3.*)
# - Added "-prefer-ipv4" to the mplayer line (videoclips starts in no time now).               - (v2.3.*)
# - Search and play videos at 480p, 720p. Ex: [words] --480, [words] -A --480                  - (v2.3.*)
# - Added support to play a video at 480p even if its resolution is higher. Ex: [url] --480    - (v2.2.*)
# - Added a nice feature which prints some information about the current playing video         - (v2.2.*)
# - Added support to play videos by your order. Example: after search results, insert: 3 5 2 1 - (v2.1.*)
# - Added support for next pages of video results (press <ENTER> after search results)         - (v2.1.*)
# - Added support to continue playing searched videos, usage: "youtube-viewer [words] -A"      - (v2.1.*)
# - Added support to print counted videos and support to insert a number instead of video code - (v2.1.*)
# - Added support to search YouTube Videos in script (e.g.: youtube-viewer avatar trailer)     - (v2.0.*)
# - Added support to choose the quality only between 720p and 1080p (if it is available)       - (v2.0.*)
# - Added support for YouTube video codes (e.g.: youtube-viewer WVTWCPoUt8w)                   - (v1.0.*)
# - Added support for 720p and 1080p YouTube Videos...                                         - (v1.0.*)

# Many thanks to everyone who contributed in making this application.
#   https://github.com/trizen/youtube-viewer/graphs/contributors

# Special thanks to:
# - Army (for the bug reports and for his great ideas: https://aur.archlinux.org/packages.php?ID=37779&comments=all)
# - dhn (for adding youtube-viewer in freshports.org: http://www.freshports.org/multimedia/youtube-viewer)
# - stressat (for the great review of youtube-viewer: http://stressat.blogspot.com/2012/01/youtube-viewer.html)
# - symbianflo (for packaging youtube-viewer for Mandriva: https://abf.rosalinux.ru/symbianflo/youtube-viewer)
# - gotbletu (for the great video review of youtube-viewer: http://www.youtube.com/watch?v=FnJ67oAxVQ4)
# - Julian Ospald (for adding youtube-viewer in the gentoo portage tree: http://packages.gentoo.org/package/net-misc/youtube-viewer)
# - 666philb (for packaging gtk-youtube-viewer for Puppy Linux: http://www.murga-linux.com/puppy/viewtopic.php?t=76835)
# - Kevin Lemonnier (for adding proxy support - https://github.com/trizen/youtube-viewer/pull/18)
# - Georgo (for adding the 'Watch Later' feature - https://github.com/trizen/youtube-viewer/pull/44)
# - Daniel Wallace (for adding youtube-viewer in the Arch Official Community repository - https://www.archlinux.org/packages/community/any/youtube-viewer/)
# - NTmatter (for adding reverse range support - https://github.com/trizen/youtube-viewer/pull/2)
# - Andrew (for a nice review of (gtk-)youtube-viewer: http://www.webupd8.org/2015/02/youtube-viewer-complete-youtube-client.html)
# - AnyComputer (for a complete french review of gtk-youtube-viewer: https://www.youtube.com/watch?v=6-qbdDUlBqg)
# - Jookia (for fixing the proxy system and improving the support for encrypted videos - https://github.com/Jookia)

=head1 NAME

youtube-viewer - YouTube from command line.

See: youtube-viewer --help
     youtube-viewer --tricks
     youtube-viewer --examples
     youtube-viewer --stdin-help

=head1 LICENSE AND COPYRIGHT

Copyright 2010-2017 Trizen.

This program is free software; you can redistribute it and/or modify it
under the terms of either: the GNU General Public License as published
by the Free Software Foundation; or the Artistic License.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

See L<https://dev.perl.org/licenses/> for more information.

=cut

use utf8;
use 5.016;

use warnings;
no warnings 'once';

my $DEVEL;    # true in devel mode
use if ($DEVEL = 0), lib => qw(../lib);    # devel mode

no if $] >= 5.018, warnings => 'experimental::smartmatch';

use WWW::YoutubeViewer v3.2.7;
use WWW::YoutubeViewer::RegularExpressions;

use File::Spec::Functions qw(
  catdir
  catfile
  curdir
  path
  rel2abs
  tmpdir
  file_name_is_absolute
  );

binmode(STDOUT, ':utf8');

my $appname  = 'Youtube Viewer';
my $version  = $WWW::YoutubeViewer::VERSION;
my $execname = 'youtube-viewer';

# A better <STDIN> support:
require Term::ReadLine;
my $term = Term::ReadLine->new("$appname $version");

# Developer key
my $key = 'aXalQYmzI8gPkMSLyMhpApfMAiU2b23Qz2nE3mq';

sub VIDEO_PART () { 'contentDetails,statistics' }
sub EXTRA_VIDEO_PART () { join(',', 'snippet', VIDEO_PART()) }

# Options (key=>value) goes here
my %opt;
my $term_width = 80;

# Keep track of watched videos by their ID
my %watched_videos;

# Unchangeable data goes here
my %constant = (win32 => ($^O eq 'MSWin32' ? 1 : 0));    # doh

my $home_dir;
my $xdg_config_home = $ENV{XDG_CONFIG_HOME};

if ($xdg_config_home and -d -w $xdg_config_home) {
    require File::Basename;
    $home_dir = File::Basename::dirname($xdg_config_home);

    if (not -d -w $home_dir) {
        $home_dir = curdir();
    }
}
else {
    $home_dir =
         $ENV{HOME}
      || $ENV{LOGDIR}
      || ($constant{win32} ? '\Local Settings\Application Data' : ((getpwuid($<))[7] || `echo -n ~`));

    if (not -d -w $home_dir) {
        $home_dir = curdir();
    }

    $xdg_config_home = catdir($home_dir, '.config');
}

# Configuration dir/file
my $config_dir = catdir($xdg_config_home, $execname);
my $config_file         = catfile($config_dir, "$execname.conf");
my $authentication_file = catfile($config_dir, 'reg.dat');
my $history_file        = catfile($config_dir, 'history.txt');

if (not -d $config_dir) {
    require File::Path;
    File::Path::make_path($config_dir)
      or warn "[!] Can't create dir '$config_dir': $!";
}

sub which_command {
    my ($cmd) = @_;

    if (file_name_is_absolute($cmd)) {
        return $cmd;
    }

    state $paths = [path()];
    foreach my $path (@{$paths}) {
        if (-e (my $cmd_path = catfile($path, $cmd))) {
            return $cmd_path;
        }
    }
    return;
}

# Main configuration
my %CONFIG = (

    video_players => {
                      vlc => {
                              cmd     => q{vlc},
                              srt     => q{--sub-file *SUB*},
                              audio   => q{--input-slave *AUDIO*},
                              fs      => q{--fullscreen},
                              arg     => q{--quiet --play-and-exit --no-video-title-show --input-title-format *TITLE*},
                              novideo => q{--intf dummy --novideo},
                             },
                      mpv => {
                              cmd     => q{mpv},
                              srt     => q{--sub-file *SUB*},
                              audio   => q{--audio-file *AUDIO*},
                              fs      => q{--fullscreen},
                              arg     => q{--really-quiet --title *TITLE* --no-ytdl},
                              novideo => q{--no-video},
                             },
                      mplayer => {
                                  cmd     => q{mplayer},
                                  srt     => q{-sub *SUB*},
                                  audio   => q{-audiofile *AUDIO*},
                                  fs      => q{-fs},
                                  arg     => q{-prefer-ipv4 -really-quiet -title *TITLE*},
                                  novideo => q{-novideo},
                                 },
                     },

    video_player_selected => (
        $constant{win32}
        ? 'mplayer'
        : undef            # autodetect it later
    ),

    combine_multiple_videos => 0,

    # YouTube options
    dash_support    => 1,
    dash_mp4_audio  => 1,
    maxResults      => 20,
    resolution      => 'original',
    videoDefinition => undef,
    videoDimension  => undef,
    videoLicense    => undef,
    safeSearch      => undef,
    videoCaption    => undef,
    videoDuration   => undef,
    videoSyndicated => undef,
    publishedBefore => undef,
    publishedAfter  => undef,
    order           => undef,

    subscriptions_order => 'relevance',

    hl          => 'en_US',
    cats_region => 'us',

    # URI options
    youtube_video_url => 'https://www.youtube.com/watch?v=%s',

    # Subtitle options
    srt_languages => ['en', 'es'],
    captions_dir  => tmpdir(),
    get_captions  => 1,
    auto_captions => 0,
    copy_caption  => 0,
    cache_dir     => undef, # will be defined later

    # Others
    http_proxy           => undef,
    env_proxy            => 1,
    confirm              => 0,
    debug                => 0,
    page                 => 1,
    colors               => $constant{win32} ^ 1,
    clobber              => 0,
    skip_if_exists       => 0,
    fat32safe            => $constant{win32},
    fullscreen           => 0,
    results_with_details => 0,
    results_with_colors  => 0,
    results_fixed_width  => 0,
    interactive          => 1,
    get_term_width       => $constant{win32} ^ 1,
    download_with_wget   => 0,
    download_in_parallel => 0,
    thousand_separator   => q{,},
    downloads_dir        => curdir(),
    keep_original_video  => 0,
    download_and_play    => 0,
    autohide_watched     => 0,
    highlight_watched    => 0,
    highlight_color      => 'bold',
    remove_played_file   => 0,
    history              => 0,
    history_limit        => 10_000,
    history_file         => $history_file,
    convert_cmd          => q{ffmpeg -i *IN* *OUT*},
    convert_to           => undef,

    video_filename_format => q{*FTITLE*.*FORMAT*},
);

local $SIG{__WARN__} = sub { warn @_; ++$opt{_error} };

my %MPLAYER;    # will store video player arguments

my $base_options = <<'BASE';
# Base
[keywords]        : search for YouTube videos
[youtube-url]     : play a video by YouTube URL
:v(ideoid)=ID     : play videos by YouTube video IDs
[playlist-url]    : display videos from a playlistURL
:playlist=ID      : display videos from a playlistID
:lectures=ID      : display lectures from a categoryID
:course=ID        : display lectures from a courseID
:courses=ID       : display courses of lectures from a categoryID
BASE

my $action_options = <<'ACTIONS';
# Actions
:login            : will prompt you for login
:logout           : will delete the authentication key
ACTIONS

my $control_options = <<'CONTROL';
# Control
:n(ext)           : get the next page of results
:b(ack)           : get the previous page of results
CONTROL

my $other_options = <<'OTHER';
# Others
:r(eturn)         : return to previous section
:refresh          : refresh the current list of results
:dv=i             : display the data structure of result i
-argv -argv2=v    : apply some arguments (e.g.: -u=google)
:reset, :reload   : restart the application
:q, :quit, :exit  : close the application
OTHER

my $notes_options = <<'NOTES';
NOTES:
 1. You can specify more options in a row, separated by spaces.
 2. A stdin option is valid only if it begins with '=', ';' or ':'.
 3. Quoting a group of space separated keywords or option-values,
    the group will be considered a single keyword or a single value.
NOTES

my $general_help = <<"HELP";

$action_options
$control_options
$other_options
$notes_options
Examples:
     3                 : select the 3rd result
    -V funny cats      : search for videos
    -p classical music : search for playlists of videos
HELP

my $playlists_help = <<"PL_HELP" . $general_help;

# Playlists
:pp=i,i           : play videos from the selected playlists
PL_HELP

my $comments_help = <<"COM_HELP" . $general_help;

# Comments
:c(omment)        : send a comment to this video
COM_HELP

my $complete_help = <<"STDIN_HELP";

$base_options
$control_options
$action_options
# YouTube
:i(nfo)=i,i       : display more information
:d(ownload)=i,i   : download the selected videos
:c(omments)=i     : display video comments
:r(elated)=i      : display related videos
:a(uthor)=i       : display author's latest uploads
:p(laylists)=i    : display author's playlists
:ps=i, :s2p=i,i   : save videos to a post-selected playlist
:subscribe=i      : subscribe to author's channel
:(dis)like=i      : like or dislike a video
:fav(orite)=i     : favorite a video

# Playing
<number>          : play the corresponding video
3-8, 3..8         : same as 3 4 5 6 7 8
8-3, 8..3         : same as 8 7 6 5 4 3
8 2 12 4 6 5 1    : play the videos in your order
10..              : play all the videos onwards from 10
:q(ueue)=i,i,...  : enqueue videos for playing them later
:pq, :play-queue  : play the enqueued videos (if any)
:anp, :nnp        : auto-next-page, no-next-page
:play=i,i,...     : play a group of selected videos
:regex=my?[regex] : play videos matched by a regex (/i)
:kregex=KEY,RE    : play videos if the value of KEY matches the RE

$other_options
$notes_options
** Examples:
:regex="\\w \\d" -> play videos matched by a regular expression.
:info=1,4      -> show informations for the first and 4th video.
:d18-20,1,2    -> download the videos: 18, 19, 20, 1 and 2.
-u=google -D   -> list videos from google with extra details.
3 4 :next 9    -> play the 3rd and 4th videos from the current
                  page, go to the next page and play the 9th video.
STDIN_HELP

{
    my $config_documentation = <<"EOD";
#!/usr/bin/perl

# $appname $version - configuration file

EOD

    sub dump_configuration {
        state $x = require Data::Dump;
        open my $config_fh, '>', $config_file
          or do { warn "[!] Can't open '${config_file}' for write: $!"; return };
        my $dumped_config = q{our $CONFIG = } . Data::Dump::pp(\%CONFIG) . "\n";
        print $config_fh $config_documentation, $dumped_config;
        close $config_fh;
    }
}

if (not -e $config_file or -z _ or $opt{reconfigure}) {
    dump_configuration();
}

our $CONFIG;
require $config_file;    # Load the configuration file

if (ref $CONFIG ne 'HASH') {
    die "[ERROR] Invalid configuration file!\n\t\$CONFIG is not an HASH ref!";
}

# Add audio support to players (backwards compatibility)
while (my ($player, $data) = each %{$CONFIG->{video_players}}) {
    if (    exists $CONFIG{video_players}{$player}
        and not exists $data->{audio}
        and exists $CONFIG{video_players}{$player}{audio}) {
        $data->{audio} = $CONFIG{video_players}{$player}{audio};
    }
}

# Get valid config keys
my @valid_keys = grep { exists $CONFIG{$_} } keys %{$CONFIG};
@CONFIG{@valid_keys} = @{$CONFIG}{@valid_keys};

{
    my $update_config = 0;

    # Define the cache directory
    if (not defined $CONFIG{cache_dir}) {

        my $cache_dir =
          ($ENV{XDG_CACHE_HOME} and -d -w $ENV{XDG_CACHE_HOME})
          ? $ENV{XDG_CACHE_HOME}
          : catdir($home_dir, '.cache');

        if (not -d -w $cache_dir) {
            $cache_dir = catdir(curdir(), '.cache');
        }

        $CONFIG{cache_dir} = catdir($cache_dir, 'youtube-viewer');
        $update_config = 1;
    }

    # Locating a video player
    if (not defined $CONFIG{video_player_selected}) {
        foreach my $key (sort keys %{$CONFIG{video_players}}) {
            if (defined(my $abs_player_path = which_command($CONFIG{video_players}{$key}{cmd}))) {
                $CONFIG{video_players}{$key}{cmd} = $abs_player_path;
                $CONFIG{video_player_selected}    = $key;
                $update_config                    = 1;
                last;
            }
        }
    }

    if ($update_config or not \%CONFIG ~~ $CONFIG) {
        dump_configuration();
    }
}

# Create the cache directory (if needed)
if (not -d $CONFIG{cache_dir}) {
    require File::Path;
    File::Path::make_path($CONFIG{cache_dir})
      or warn "[!] Can't create dir `$CONFIG{cache_dir}': $!";
}

@opt{keys %CONFIG} = values(%CONFIG);

if ($opt{history}) {

    # Create the history file.
    if (not -e $opt{history_file}) {
        open my $fh, '>', $opt{history_file}
          or warn "[!] Can't create the history file `$opt{history_file}': $!";
    }

    # Add history to Term::ReadLine
    $term->ReadHistory($opt{history_file});

    # All history entries
    my @entries = $term->history_list;

    # Rewrite the history file, when the history_limit has been reached.
    if ($opt{history_limit} > 0 and @entries > $opt{history_limit}) {

        # Try to create a backup, first
        require File::Copy;
        File::Copy::cp($opt{history_file}, "$opt{history_file}.bak");

        if (open my $fh, '>', $opt{history_file}) {
            say {$fh} join("\n", @entries[(@entries - $opt{history_limit} + rand($opt{history_limit} >> 1)) .. $#entries]);
            close $fh;
        }
    }
}

{
    my $i = length $key;
    $key =~ s/(.{$i})(.)/$2$1/g while --$i;
}

my $yv_obj = WWW::YoutubeViewer->new(
                                     escape_utf8         => 1,
                                     key                 => $key,
                                     config_dir          => $config_dir,
                                     cache_dir           => $opt{cache_dir},
                                     lwp_env_proxy       => $opt{env_proxy},
                                     authentication_file => $authentication_file,
                                    );

{
    $yv_obj->set_client_id('923751928481.apps.googleusercontent.com');
    $yv_obj->set_client_secret("\26/Ae]3\b\6\x186a:*#0\32\t\f\n\27\17GC`" ^ substr($key, -24));
    $yv_obj->set_redirect_uri('urn:ietf:wg:oauth:2.0:oob');
}

require WWW::YoutubeViewer::Utils;
my $yv_utils = WWW::YoutubeViewer::Utils->new(youtube_url_format => $opt{youtube_video_url},
                                              thousand_separator => $opt{thousand_separator},);

{    # Apply the configuration file
    my %temp = %CONFIG;
    apply_configuration(\%temp);
}

#---------------------- YOUTUBE-VIEWER USAGE ----------------------#
sub help {
    my $eqs = q{=} x 30;

    local $" = ', ';
    print <<"HELP";
\n  $eqs \U$appname\E $eqs
\t\t\t\t\t\t by Trizen (trizenx\@gmail.com)

usage: $execname [options] ([url] | [keywords])

== Base ==
   [URL]             : play an YouTube video by URL
   [keywords]        : search for YouTube videos
   [playlist URL]    : display a playlist of YouTube videos


== YouTube Options ==

 * Video tops:
   -t  --tops           : display the YouTube video tops
       --tops=all       : display all time YouTube video tops
   -r  --region=s       : list top videos for a specific region
   -M  --movies         : display the YouTube category of movies

 * Categories
   -c  --categories     : display the available YouTube categories
       --edu-categories : display the YouTube EDU categories
       --course-id=s    : list the video lectures from a course_ID
   -hl --catlang=s      : language for categories (default: en_US)
       --cats-region=s  : region code for categories (default: us)

 * Videos
   -uv --user-vid=s     : list videos uploaded by a specific user
   -cv --channel-vid=s  : list videos uploaded to a specific channel
   -uf --user-fav=s     : list the videos favorited by a specific user
   -id --videoids=s,s   : play YouTube videos by their IDs
   -rv --related=s      : show related videos for a video ID or URL
       --search=s       : search for YouTube videos (default mode)

 * Playlists
   -p  --playlists    : search for playlists of videos
       --pid=s        : list a playlist of videos by playlistID
       --pp=s,s       : play the videos from the given playlist IDs
       --ps=s         : add videos by ID or URL to a post-selected playlist
                        or in a given playlistID specified with `--pid`
       --position=i   : the position in a playlist where to add a video
   -up --user-pl=s    : list the playlists created by a specific user
   -cp --channel-pl=s : list the playlists belonging to a specific channel ID

 * Channels
   --channels         : search for Youtube channels

 * Shows
   -us --user-shows=s : display the shows belonging to a user
       --seasons=s    : get and print the seasons of a YouTube show_ID
       --episodes=s   : get and print the episodes for a show season_ID
       --clips=s      : get and print the clips for a show season_ID

* Comments
   --comments=s         : display comments for a YouTube video by ID or URL

 * Filtering
   --author=s           : search in videos uploaded by a specific user
   --channel-id=s       : search in videos belonging to a specific channel ID
   --duration=s         : filter search results based on video length
                          valid values are: short, medium, long
   --caption=s          : only videos with/without closed captions
                          valid values are: true, false
   --category=i         : search only for videos in a specific category ID
   --safe-search=s      : YouTube will skip restricted videos for your location
                          valid values are: none, moderate, strict
   --order=s            : order the results using a specific sorting method
                          valid values: date rating viewCount title videoCount
   --within=s           : show only videos uploaded within the specified time
                          valid values are: Nd, Nm, Ny, where N is a number
   --hd!                : search only for videos available in at least 720p
   --vd=s               : set the video definition (any, high or standard)
   --page=i             : get results starting with a specific page
   --results=i          : how many results to display per page (max: 50)
   -2  -3  -4  -7  -1   : resolutions: 240p, 360p, 480p, 720p and 1080p
   --resolution=s       : supported resolutions: original, 2160p, 1440p,
                          1080p, 720p, 480p, 360p, 240p, 144p, audio.

 * Account
   --login              : will prompt for authentication (OAuth 2.0)
   --logout             : will delete the authentication key

 * [GET] Personal
   -F  --favorites:s     : show the latest favorited videos *
   -S  --subscriptions:s : show the subscribed channels *
   -SV --subs-videos:s   : show the subscription videos *
       --subs-order=s    : change the subscription order
                           valid values: alphabetical, relevance, unread
   -L  --likes           : show the videos that you liked on YouTube *
       --dislikes        : show the videos that you disliked on YouTube *

* [POST] Personal
   --subscribe=s        : subscribe to a channel *
   --user-subscribe=s   : subscribe to a channel via username *
   --favorite=s         : favorite a YouTube video by URL or ID *
   --like=s             : send a 'like' rating to a video URL or ID *
   --dislike=s          : send a 'dislike' rating to a video URL or ID *


== Player Options ==

 * Arguments
   -f  --fullscreen!  : set the fullscreen mode for the selected video player
   -n  --novideo!     : play the music only without a video in the foreground
   --vo=s             : specify the video output for MPlayer
   --af=s             : specify an audio filter for MPlayer
   --append-arg=s     : append some command-line parameters to the media player
   --video-player=s   : select a video player to stream videos
                        available players: @{[keys %{$CONFIG->{video_players}}]}


== Download Options ==

 * Download
   -d  --download!       : activate the download mode
   -dp --downl-play!     : play the video after download (with -d)
   -rp --rem-played!     : delete a local video after played (with -dp)
       --wget-dl!        : download videos with wget (default: LWP)
       --dl-parallel     : download multiple videos at once
       --clobber!        : overwrite an existent video (with -d)
       --skip-if-exists! : don't download videos which already exists (with -d)
       --copy-caption!   : copy and rename the caption for downloaded videos
       --downloads-dir=s : downloads directory (set: '$opt{downloads_dir}')
       --filename=s      : set a custom format for the video filename (see: -T)
       --fat32safe!      : makes filenames FAT32 safe (includes Unicode)

 * Convert
   --convert-cmd=s      : command for converting videos after download
                          which include the *IN* and *OUT* tokens
   --convert-to=s       : convert video to a specific format (with -d)
   --keep-original!     : keep the original video after converting


== Other Options ==

 * Behavior
   -A  --all!            : play all the video results in order
   -B  --backwards!      : play all video results in reverse order
   -s  --shuffle!        : shuffle the results of videos and playlists
   -I  --interactive!    : interactive mode, prompting for user input
       --std-input=s     : use this value as the first standard input
       --max-seconds=i   : ignore videos longer than i seconds
       --min-seconds=i   : ignore videos shorter than i seconds
       --combine-multi!  : combine multiple videos into one play instance
       --get-term-width! : allow $execname to read your terminal width
       --autohide!       : automatically hide watched videos
       --highlight!      : remember and highlight selected videos
       --confirm!        : show a confirmation message after each play

 * Closed-captions
   --get-captions!      : download the closed captions for videos
   --auto-captions!     : include or exclude auto-generated captions
   --captions-dir=s     : the directory where to download the .srt files

 * Config
   -U  --update-config! : update the configuration file before exit

 * Output
   -C  --colorful!      : use colors to delimit the video results
   -D  --details!       : a new look for the results, with more details
   -W  --fixed-width!   : adjust the results to fit inside the term width
   -i  --info=s         : show some info for a videoID or URL
   -e  --extract=s      : extract information from videos (see: -T)
       --extract-file=s : extract the information from videos in this file
       --dump=format    : dump metadata information in `videoID.format` files
                          valid formats: json, perl
   -q  --quiet          : do not display any warning
       --really-quiet   : do not display any warning or output
       --escape-info!   : quotemeta() the fields of the `--extract`
       --use-colors!    : enable or disable the ANSI colors for text

 * Other
   --http_proxy=s       : HTTP proxy to use, format 'http://domain.tld:port/'.
                          If authentication required,
                          use 'http://user:pass\@domain.tld:port/'
   --dash!              : include or exclude the DASH itags
   --dash-mp4a!         : include or exclude the itags for MP4 audio streams


Help options:
   -T  --tricks         : show more 'hidden' features of $execname
   -E  --examples       : show some useful usage examples for $execname
   -H  --stdin-help     : show the valid stdin options for $execname
   -v  --version        : print version and exit
   -h  --help           : print help and exit
       --debug:[1,2]    : see behind the scenes

NOTES:
    *    -> requires authentication
    !    -> the argument can be negated with '--no'
    =i   -> requires an integer argument
    =s   -> requires an argument
    :s   -> can take an optional argument
    =s,s -> can take more arguments separated by commas

HELP
    main_quit(0);
}

sub wrap_text {
    my (%args) = @_;

    state $x = require Text::Wrap;
    local $Text::Wrap::columns = ($args{columns} || $term_width) - 8;

    my $text = "@{$args{text}}";
    $text =~ tr{\r}{}d;

    return eval { Text::Wrap::wrap($args{i_tab}, $args{s_tab}, $text) } // $text;
}

sub tricks {
    print <<"TRICKS";

                == youtube-viewer -- tips and tricks ==

-> Playing videos
 > To stream the videos in other players, you need to change the
   configuration file. Where it says "video_player_selected", change it
   to any player which is defined inside the "video_players" hash.

-> Arguments
 > Almost all boolean arguments can be negated with a "--no-" prefix.
 > Arguments that require an ID/URL, you can specify more than one,
   separated by whitespace (quoted), or separated by commas.

-> My channel
 > Starting with version 3.2.1, it's possible to use the string "mine"
   in place where a channel ID is required. Doing this, "mine" will be
   replaced with your channel ID. (requires authentication)

   Examples:
        $execname --channel-playlists=mine
        $execname --channel-videos=mine
        $execname --likes=mine
        $execname --favorites=mine

-> More STDIN help:
 > ":r", ":return" will return to the previous section.
   For example, if you search for playlists, then select a playlist
   of videos, inserting ":r" will return back to the playlist results.
   Also, for the previous page, you can insert ':b', but ':r' is faster!

 > "6" (quoted) or -V=6 will search for videos with the keyword '6'.

 > If a stdin option is followed by one or more digits, the equal sign,
   which separates the option from value, can be omitted.
   For example:
        :i2,4  is equivalent with :i=2,4
        :d1-5  is equivalent with :d=1,2,3,4,5
        :c10   is equivalent with :c=10

 > When more videos are selected to play, you can stop them by
   pressing CTRL+C. $execname will return to the previous section.

 > Space inside the values of STDIN options, can be either quoted
   or backslashed.
   For example:
        :re=video\\ title     ==     :re="video title"

 > ":anp" stands for the "Auto Next Page". How do we use it?
   Well, let's search for some videos. Now, if we'd want to play
   only the videos matched by a regex, we'd say :re="REGEX".
   But, what if we'd want to play the videos from the next pages too?
   In this case, ":anp" is your friend. Use it wisely!

-> Special tokens:

  *ID*          : the YouTube video ID
  *AUTHOR*      : the author name of the video
  *CHANNELID*   : the channel ID of the video
  *RESOLUTION*  : the resolution of the video
  *VIEWS*       : the number of views
  *LIKES*       : the number of likes
  *DISLIKES*    : the number of dislikes
  *COMMENTS*    : the number of comments
  *DURATION*    : the duration of the video in seconds
  *DIMENSION*   : the dimension of the video (2D or 3D)
  *DEFINITION*  : the definition of the video (HD or SD)
  *TIME*        : the duration of the video in HH::MM::SS
  *TITLE*       : the title of the video
  *FTITLE*      : the title of the video (filename safe)
  *DESCRIPTION* : the description of the video

  *URL*      : the YouTube URL of the video
  *ITAG*     : the itag value of the video
  *FORMAT*   : the extension of the video (without the dot)

  *CAPTION*  : true if the video has caption.
  *SUB*      : the local subtitle file (if any)
  *AUDIO*    : the audio URL of the video (only in DASH mode)
  *VIDEO*    : the video URL of the video (it might not contain audio)
  *AOV*      : audio URL (if any) or video URL (in this order)

-> Special escapes:
    \\t                  tab
    \\n                  newline
    \\r                  return
    \\f                  form feed
    \\b                  backspace
    \\a                  alarm (bell)
    \\e                  escape

-> Extracting information from videos:
 > Extracting information can be achieved by using the "--extract" command-line
   option which takes a given format as its argument, which is defined by using
   special tokens, special escapes or literals.

   Example:
     $execname --no-interactive --extract '*TITLE* (*ID*)' [URL]

-> Configuration file: $config_file

-> Donations gladly accepted:
    https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=75FUVBE6Q73T8

TRICKS
    main_quit(0);
}

sub examples {
    print <<"EXAMPLES";
==== COMMAND LINE EXAMPLES ====

Command: $execname -A -n -4 russian music -category=10
Results: play all the video results (-A)
         only audio, no video (-n)
         quality 480p (-4)
         search for "russian music"
         in the "10" category, which is the Music category.
         -A will include the videos from the next pages as well.

Command: $execname -comments=https://www.youtube.com/watch?v=U6_8oIPFREY
Results: show video comments for a specific video URL or videoID

Command: $execname -results=5 -up=khanacademy -D
Results: set 5 results,
         get playlists created by a specific user
         and print them with details (-D)

Command: $execname -author=UCBerkeley atom
Results: search only in videos uploaded by a specific author

Command: $execname -S=vsauce
Results: get the video subscriptions for a username

Command: $execname --page=2 -u=Google
Results: show latest videos uploaded by Google,
         starting with the page number 2.

Command: $execname --category=10 --tops
Results: show today tops for Music category.

Command: $execname --tops=all --region=JP
Results: show all time tops for the Japan country.

Command: $execname --tops --region=RU --category=Music
Results: show today tops of RUssian Music

Command: $execname cats -order=viewCount -duration=short
Results: search for 'cats' videos, ordered by ViewCount and short length.

Command: $execname --channels russian music
Results: search for channels.

Command: $execname --uf=Google
Results: show latest videos favorited by a user.


==== USER INPUT EXAMPLES ====

A STDIN option can begin with ':', ';' or '='.

Command: <ENTER>, :n, :next, CTRL+D
Results: get the next page of results.

Command: :b, :back (:r, :return)
Results: get the previous page of results.

Command: :i4..6, :i7-9, :i20-4, :i2, :i=4, :info=4
Results: show video informations for the selected videos.

Command: :d5,2, :d=3, :download=8
Results: download the selected videos.

Command: :c2, :comments=4
Results: show comments for a selected video.

Command: :r4, :related=6
Results: show related videos for a selected video.

Command: :a14, :author=12
Results: show videos uploaded by the author who uploaded the selected video.

Command: :p9, :playlists=14
Results: show playlists created by the author who uploaded the selected video.

Command: :subscribe=7
Results: subscribe to the author's channel who uploaded the selected video.

Command: :like=2, :dislike=4,5
Results: like or dislike the selected videos.

Command: :course=EC7AEDF86AABA1AA9A
Results: list videos lectures from course ID.

Command: :courses=285
Results: list courses of lectures from EDU category ID.

Command: :lectures=361
Results: list video lectures from EDU category ID.

Command: :fav=4, :favorite=3..5
Results: favorite the selected videos.

Command: 3, 5..7, 12-1, 9..4, 2 3 9
Results: play the selected videos.

Command: :q3,5, :q=4, :queue=3-9
Results: enqueue the selected videos to play them later.

Command: :pq, :play-queue
Results: play the videos enqueued by the :queue option.

Command: :re="^Google Tricks"
Results: play videos matched by a regex.
Example: valid title: "Google Tricks & Easter eggs"

Command: :regex="Google.*part \\d+/\\d+"
Example: valid title: "The GOOGLE company (part 1/4)"

Command: :kregex=author,^google\$
Results: play only the videos uploaded by google.

Command: :kre=category,"^(?:People & Blogs|Entertainment)\$"
Results: play only the videos from the specified categories.

Command: :kre=views,"^(\\d+)\\z(?(?{ \$1 > 1000 })(?=)|(?!))"
Results: play only the videos which have more than 1000 of views.

Command: :anp 1 2 3
Results: play the first three videos from every page.

Command: :r, :return
Results: return to the previous section.
EXAMPLES
    main_quit(0);
}

sub stdin_help {
    print $complete_help;
    main_quit(0);
}

# Print version
sub version {
    print "YouTube Viewer $version\n";
    main_quit(0);
}

sub apply_configuration {
    my ($opt, $keywords) = @_;

    if ($yv_obj->get_debug == 2
        or (defined($opt->{debug}) && $opt->{debug} == 2)) {
        state $x = require Data::Dump;
        say "=>> Options with keywords: <@{$keywords}>";
        Data::Dump::pp($opt);
    }

    # ... BASIC OPTIONS ... #
    if (delete $opt->{quiet}) {
        close STDERR;
    }

    if (delete $opt->{really_quiet}) {
        close STDERR;
        close STDOUT;
    }

    # ... YOUTUBE OPTIONS ... #
    foreach my $option_name (
                             qw(
                             videoCaption maxResults order
                             videoDefinition videoCategoryId
                             videoDimension videoDuration
                             videoEmbeddable videoLicense
                             videoSyndicated channelId
                             publishedAfter publishedBefore
                             safeSearch regionCode debug hl
                             http_proxy page subscriptions_order
                             )
      ) {

        if (defined $opt->{$option_name}) {
            my $code      = \&{"WWW::YoutubeViewer::set_$option_name"};
            my $value     = delete $opt->{$option_name};
            my $set_value = $yv_obj->$code($value);

            if (not defined($set_value) or $set_value ne $value) {
                warn "\n[!] Invalid value <$value> for option <$option_name>\n";
            }
        }
    }

    if (defined $opt->{hd}) {
        $yv_obj->set_videoDefinition(delete($opt->{hd}) ? 'high' : 'any');
    }

    if (defined $opt->{author}) {
        my $username = delete $opt->{author};
        $yv_obj->set_channelId($yv_obj->channel_id_from_username($username) // $username);
    }

    if (defined $opt->{within}) {
        my $value = delete $opt->{within};

        if ($value =~ /^\s*(\d+(?:\.\d+)?)([dmy])/i) {
            my $date = $yv_utils->period_to_date($1, $2);
            $yv_obj->set_publishedAfter($date);
        }
        else {
            warn "\n[!] Invalid value <$value> for option `--within`!\n";
        }
    }

    if (defined $opt->{more_results}) {
        $yv_obj->set_maxResults(delete($opt->{more_results}) ? 50 : $CONFIG{maxResults});
    }

    if (delete $opt->{authenticate}) {
        authenticate();
    }

    if (delete $opt->{logout}) {
        logout();
    }

    # ... OTHER OPTIONS ... #
    if (defined $opt->{extract_info_file}) {
        open my $fh, '>:utf8', delete($opt->{extract_info_file});
        $opt{extract_info_fh} = $fh;
    }

    if (defined $opt->{colors}) {
        $opt{_colors} = $opt->{colors};
        if (delete $opt->{colors}) {
            state $x = require Term::ANSIColor;
            *colored    = \&Term::ANSIColor::colored;
            *colorstrip = \&Term::ANSIColor::colorstrip;
        }
        else {
            *colored    = sub { $_[0] };
            *colorstrip = sub { $_[0] };
        }
    }

    # ... SUBROUTINE CALLS ... #
    if (defined $opt->{subscribe_channel}) {
        subscribe_to_channels(split(/[,\s]+/, delete $opt->{subscribe_channel}));
    }

    if (defined $opt->{subscribe_username}) {
        subscribe_to_usernames(split(/[,\s]+/, delete $opt->{subscribe_username}));
    }

    if (defined $opt->{favorite_video}) {
        favorite_videos(split(/[,\s]+/, delete $opt->{favorite_video}));
    }

    if (defined $opt->{playlist_save}) {
        my @ids = split(/[,\s]+/, delete $opt->{playlist_save});
        if (defined $opt->{playlist_id}) {
            save_to_playlist(delete $opt->{playlist_id}, @ids);
        }
        else {
            select_and_save_to_playlist(@ids);
        }
    }

    if (defined $opt->{like_video}) {
        rate_videos('like', split(/[,\s]+/, delete $opt->{like_video}));
    }

    if (defined $opt->{dislike_video}) {
        rate_videos('dislike', split(/[,\s]+/, delete $opt->{dislike_video}));
    }

    if (defined $opt->{play_video_ids}) {
        get_and_play_video_ids(split(/[,\s]+/, delete $opt->{play_video_ids}));
    }

    if (defined $opt->{play_playlists}) {
        get_and_play_playlists(split(/[,\s]+/, delete $opt->{play_playlists}));
    }

    if (defined $opt->{playlist_id}) {
        my $playlistID = delete($opt{playlist_id});
        get_and_print_videos_from_playlist($playlistID);
    }

    if (defined $opt->{search_playlists}) {
        my $value = delete($opt->{search_playlists});
        if ($value =~ /$valid_playlist_id_re/ and not @{$keywords}) {
            get_and_print_videos_from_playlist($value);
        }
        else {
            print_playlists($yv_obj->search_playlists([$value, @{$keywords}]));
        }
    }

    if (defined $opt->{course_id}) {
        get_and_print_videos_from_course(delete $opt->{course_id});
    }

    if (defined $opt->{search_videos}) {
        my $value = delete $opt->{search_videos};
        print_videos($yv_obj->search_videos([$value, @{$keywords}]));
    }

    if (defined $opt->{search_channels}) {
        my $value = delete $opt->{search_channels};
        print_channels($yv_obj->search_channels($value, @{$keywords}));
    }

    if (delete $opt->{categories}) {
        print_categories($yv_obj->video_categories($opt{cats_region}));
    }

    if (defined $opt->{show_seasons}) {
        get_and_print_content_from_show(delete $opt->{show_seasons});
    }

    if (defined $opt->{season_episodes}) {
        get_and_print_episodes_from_season_id(delete $opt->{season_episodes});
    }

    if (defined $opt->{season_clips}) {
        get_and_print_clips_from_season_id(delete $opt->{season_clips});
    }

    if (defined $opt->{user_videos}) {
        if ($opt->{user_videos} =~ /$valid_channel_id_re/) {
            print_videos($yv_obj->uploads_from_username(delete $opt->{user_videos}));
        }
        else {
            warn_invalid("username", $opt->{user_videos});
        }
    }

    if (defined $opt->{channel_id_videos}) {
        print_videos($yv_obj->uploads(delete $opt->{channel_id_videos}));
    }

    if (defined $opt->{user_shows}) {
        if ($opt->{user_shows} =~ /$valid_channel_id_re/) {
            print_shows($yv_obj->get_shows_from_username(delete $opt->{user_shows}));
        }
        else {
            warn_invalid("username", $opt->{user_shows});
        }
    }

    if (defined $opt->{subscriptions}) {
        my $username = delete $opt->{subscriptions};
        print_channels($username ? $yv_obj->subscriptions_from_username($username) : $yv_obj->subscriptions);
    }

    if (defined $opt->{subscription_videos}) {
        my $username = delete $opt->{subscription_videos};
        print_videos($username ? $yv_obj->subscription_videos_from_username($username) : $yv_obj->subscription_videos);
    }

    if (defined $opt->{related_videos}) {
        get_and_print_related_videos(split(/[,\s]+/, delete($opt->{related_videos})));
    }

    if (defined $opt->{user_playlists}) {
        print_playlists($yv_obj->playlists_from_username(delete $opt->{user_playlists}));
    }

    if (defined $opt->{channel_playlists}) {
        print_playlists($yv_obj->playlists(delete $opt->{channel_playlists}));
    }

    if (defined $opt->{favorites}) {
        my $channel_id = delete($opt->{favorites});
        print_videos(
                       $channel_id
                     ? $yv_obj->favorites($channel_id)
                     : $yv_obj->favorites
                    );
    }

    if (defined $opt->{likes}) {
        my $channel_id = delete($opt->{likes});
        print_videos(
                       $channel_id
                     ? $yv_obj->likes($channel_id)
                     : $yv_obj->my_likes
                    );
    }

    if (defined $opt->{dislikes}) {
        delete $opt->{dislikes};
        print_videos($yv_obj->my_dislikes);
    }

    if (defined $opt->{user_favorited_videos}) {
        my $username = delete $opt->{user_favorited_videos};
        if ($username =~ /$valid_channel_id_re/) {
            print_videos($yv_obj->favorites_from_username($username));
        }
        else {
            warn_invalid("username", $username);
        }
    }

    if (defined $opt->{user_liked_videos}) {
        my $username = delete $opt->{user_liked_videos};
        if ($username =~ /$valid_channel_id_re/) {
            print_videos($yv_obj->likes_from_username($username));
        }
        else {
            warn_invalid("username", $username);
        }
    }

    if (defined $opt->{video_tops}) {
        print_video_tops(time_id => (delete($opt->{video_tops}) =~ /^all/i ? 'all_time' : 'today'));
    }

    if (delete $opt->{show_movies}) {
        print_movies();
    }

    if (defined $opt->{get_comments}) {
        get_and_print_comments(split(/[,\s]+/, delete($opt->{get_comments})));
    }

    if (defined $opt->{print_video_info}) {
        get_and_print_video_info(split(/[,\s]+/, delete $opt->{print_video_info}));
    }
}

sub parse_arguments {
    my ($keywords) = @_;

    state $x = require Getopt::Long;
    state $y = Getopt::Long::Configure('no_ignore_case');

    Getopt::Long::GetOptions(

        # Main options
        'help|usage|h|?'        => \&help,
        'examples|E'            => \&examples,
        'stdin-help|shelp|sh|H' => \&stdin_help,
        'tricks|tips|T'         => \&tricks,
        'version|v'             => \&version,
        'update-config|U!'      => \&dump_configuration,

        # Resolutions
        '240p|2'  => sub { $opt{resolution} = 240 },
        '360p|3'  => sub { $opt{resolution} = 360 },
        '480p|4'  => sub { $opt{resolution} = 480 },
        '720p|7'  => sub { $opt{resolution} = 720 },
        '1080p|1' => sub { $opt{resolution} = 1080 },
        'res|resolution=s' => \$opt{resolution},

        'movies|M'                                   => \$opt{show_movies},
        'comments=s'                                 => \$opt{get_comments},
        'search|videos|V:s'                          => \$opt{search_videos},
        'video-ids|videoids|id|ids=s'                => \$opt{play_video_ids},
        'tops|video-tops|t:s'                        => \$opt{video_tops},
        'c|categories'                               => \$opt{categories},
        'ec|educategories|edu-categories'            => \$opt{educategories},
        'show-seasons|show_seasons|seasons=s',       => \$opt{show_seasons},
        'season-episodes|season_episodes|episodes=s' => \$opt{season_episodes},
        'season-clips|season_clips|clips=s'          => \$opt{season_clips},
        'channels|search-channels|search_channels:s' => \$opt{search_channels},

        'subscriptions|S:s'                 => \$opt{subscriptions},
        'subs-videos|SV:s'                  => \$opt{subscription_videos},
        'subs-order=s'                      => \$opt{subscriptions_order},
        'favorites|fv|favorited-videos|F:s' => \$opt{favorites},
        'likes|L:s'                         => \$opt{likes},
        'dislikes'                          => \$opt{dislikes},
        'subscribe=s'                       => \$opt{subscribe_channel},
        'user-subscribe=s'                  => \$opt{subscribe_username},
        'cv|channel|channel-videos=s'       => \$opt{channel_id_videos},
        'cp|channel-playlists=s'            => \$opt{channel_playlists},

        # English-UK friendly
        'favorite|favourite|favorite-video|favourite-video|fav=s' => \$opt{favorite_video},

        'login|authenticate'      => \$opt{authenticate},
        'logout'                  => \$opt{logout},
        'user|user-videos|u|uv=s' => \$opt{user_videos},
        'shows|user-shows|us=s'   => \$opt{user_shows},
        'user-playlists|up=s'     => \$opt{user_playlists},
        'user-favorites|uf=s'     => \$opt{user_favorited_videos},
        'user-likes|ul=s'         => \$opt{user_liked_videos},
        'related-videos|rl|rv=s'  => \$opt{related_videos},
        'http_proxy=s'            => \$opt{http_proxy},

        'catlang|cl|hl=s'          => \$opt{hl},
        'category|cat-id|cat=i'    => \$opt{videoCategoryId},
        'cats-region|cat-region=s' => \$opt{cats_region},
        'r|region|region-code=s'   => \$opt{regionCode},

        'orderby|order|order-by=s' => \$opt{order},
        'duration=s'               => \$opt{videoDuration},
        'within=s'                 => \$opt{within},

        'max-seconds|max_seconds=i' => \$opt{max_seconds},
        'min-seconds|min_seconds=i' => \$opt{min_seconds},

        'like=s'                     => \$opt{like_video},
        'dislike=s'                  => \$opt{dislike_video},
        'author=s'                   => \$opt{author},
        'channel-id=s'               => \$opt{channelId},
        'all|A|play-all!'            => \$opt{play_all},
        'backwards|B!'               => \$opt{play_backwards},
        'input|std-input=s'          => \$opt{std_input},
        'use-colors|colors|colored!' => \$opt{colors},

        'playlists|p|pl|playlist:s' => \$opt{search_playlists},
        'pid|playlist-id=s'         => \$opt{playlist_id},
        'course|course-id=s'        => \$opt{course_id},
        'play-playlists|pp=s'       => \$opt{play_playlists},
        'debug:1'                   => \$opt{debug},
        'download|dl|d!'            => \$opt{download_video},
        'safe-search|safeSearch=s'  => \$opt{safeSearch},
        'vd|video-definition=s'     => \$opt{videoDefinition},
        'hd|high-definition!'       => \$opt{hd},
        'I|interactive!'            => \$opt{interactive},
        'convert-to|convert_to=s'   => \$opt{convert_to},
        'keep-original-video!'      => \$opt{keep_original_video},
        'e|extract|extract-info=s'  => \$opt{extract_info},
        'extract-file=s'            => \$opt{extract_info_file},
        'escape-info!'              => \$opt{escape_info},

        'dump=s' => sub {
            my (undef, $format) = @_;
            $opt{dump} = (
                ($format =~ /json/i) ? 'json' : ($format =~ /perl/i) ? 'perl' : do {
                    warn "[!] Invalid format <<$format>> for option --dump\n";
                    undef;
                  }
            );
        },

        # MPlayer
        'player|vplayer|video-player|video_player=s' => \$opt{video_player_selected},
        'append-mplayer|append-arg|arg=s'            => \$MPLAYER{user_defined_arguments},
        'vo=s'                                       => sub { $MPLAYER{video_output} = "-vo $_[1]" },
        'af=s'                                       => sub { $MPLAYER{audio_filter} = "-af $_[1]" },

        # Others
        'colorful|colourful|C!' => \$opt{results_with_colors},
        'details|D!'            => \$opt{results_with_details},
        'fixed-width|W|fw!'     => \$opt{results_fixed_width},
        'caption=s'             => \$opt{videoCaption},
        'fullscreen|fs|f!'      => \$opt{fullscreen},
        'dash!'                 => \$opt{dash_support},
        'confirm!'              => \$opt{confirm},

        'convert-command|convert-cmd=s'      => \$opt{convert_cmd},
        'dash-m4a|dash-mp4-audio|dash-mp4a!' => \$opt{dash_mp4_audio},
        'wget-dl|wget-download!'             => \$opt{download_with_wget},
        'dl-parallel|download-in-parallel!'  => \$opt{download_in_parallel},
        'filename|filename-format=s'         => \$opt{video_filename_format},
        'dp|downl-play|download-and-play!'   => \$opt{download_and_play},
        'rp|rem-played|remove-played-file!'  => \$opt{remove_played_file},
        'clobber!'                           => \$opt{clobber},
        'info|i|video-info=s'                => \$opt{print_video_info},
        'get-term-width!'                    => \$opt{get_term_width},
        'page=i'                             => \$opt{page},
        'novideo|no-video|n!'                => \$opt{novideo},
        'autohide!'                          => \$opt{autohide_watched},
        'highlight!'                         => \$opt{highlight_watched},
        'results=i'                          => \$opt{maxResults},
        'shuffle|s!'                         => \$opt{shuffle},
        'more|m!'                            => \$opt{more_results},
        'combine-multiple-videos|combine!'   => \$opt{combine_multiple_videos},
        'pos|position=i'                     => \$opt{position},
        'ps|playlist-save=s'                 => \$opt{playlist_save},

        'quiet|q!'      => \$opt{quiet},
        'really-quiet!' => \$opt{really_quiet},

        'thousand-separator=s'           => \$opt{thousand_separator},
        'get-captions|get_captions!'     => \$opt{get_captions},
        'auto-captions|auto_captions!'   => \$opt{auto_captions},
        'copy-caption|copy_caption!'     => \$opt{copy_caption},
        'captions-dir|captions_dir=s'    => \$opt{captions_dir},
        'skip-if-exists|skip_if_exists!' => \$opt{skip_if_exists},
        'downloads-dir|download-dir=s'   => \$opt{downloads_dir},
        'fat32safe!'                     => \$opt{fat32safe},
      )
      or die "Error in command-line arguments!";

    apply_configuration(\%opt, $keywords);
}

# Parse the arguments
if (@ARGV) {
    require Encode;
    @ARGV = map { Encode::decode_utf8($_) } @ARGV;
    parse_arguments(\@ARGV);
}

for (my $i = 0 ; $i <= $#ARGV ; $i++) {
    my $arg = $ARGV[$i];

    next if chr ord $arg eq q{-};

    if (youtube_urls($arg)) {
        splice(@ARGV, $i--, 1);
    }
}

if (my @keywords = grep chr ord ne q{-}, @ARGV) {
    print_videos($yv_obj->search_videos(\@keywords));
}
elsif ($opt{interactive} and -t) {
    first_user_input();
}
elsif ($opt{interactive} and -t STDOUT and not -t) {
    print_videos($yv_obj->search_videos(scalar <STDIN>));
}
else {
    main_quit($opt{_error} || 0);
}

sub get_valid_video_id {
    my ($value) = @_;

    my $id =
        $value =~ /$get_video_id_re/   ? $+{video_id}
      : $value =~ /$valid_video_id_re/ ? $value
      :                                  undef;

    unless (defined $id) {
        warn_invalid('videoID', $value);
        return;
    }

    return $id;
}

sub get_valid_playlist_id {
    my ($value) = @_;

    my $id =
        $value =~ /$get_playlist_id_re/   ? $+{playlist_id}
      : $value =~ /$valid_playlist_id_re/ ? $value
      :                                     undef;

    unless (defined $id) {
        warn_invalid('playlistID', $value);
        return;
    }

    return $id;
}

sub apply_input_arguments {
    my ($args, $keywords) = @_;

    if (@{$args}) {
        local @ARGV = @{$args};
        parse_arguments($keywords);
    }

    return 1;
}

# Get mplayer
sub get_mplayer {
    if ($constant{win32}) {
        my $smplayer = catfile($ENV{ProgramFiles}, qw(SMPlayer mplayer mplayer.exe));

        if (not -e $smplayer) {
            warn "\n\n!!! Please install SMPlayer in order to stream YouTube videos.\n\n";
        }

        return $smplayer;    # Windows MPlayer
    }

    return 'mplayer';        # *NIX MPlayer
}

# Get term width
sub get_term_width {
    return $term_width if $constant{win32};
    $term_width = (-t STDOUT) ? ((split(q{ }, `stty size`))[1] || $term_width) : $term_width;
}

sub first_user_input {
    my @keys = get_input_for_first_time();

    state $first_input_help = <<"HELP";

$base_options
$action_options
$other_options
$notes_options
** Example:
    To search for playlists, insert: -p keywords
HELP

    if (scalar(@keys)) {
        my @for_search;
        foreach my $key (@keys) {
            if ($key =~ /$valid_opt_re/) {
                given ($1) {
                    when (general_options(opt => $_)) { }
                    when (['help', 'h']) {
                        print $first_input_help;
                        press_enter_to_continue();
                    }
                    when (['r', 'return']) {
                        return;
                    }
                    default {
                        warn_invalid('option', $_);
                        print "\n";
                        exit 1;
                    }
                }
            }
            else {
                given ($key) {
                    when (\&youtube_urls) { }    # do nothing
                    default {
                        push @for_search, $key;
                    }
                }
            }
        }

        if (scalar(@for_search) > 0) {
            print_videos($yv_obj->search_videos(\@for_search));
        }
        else {
            __SUB__->();
        }
    }
    else {
        __SUB__->();
    }
}

sub get_quotewords {
    state $x = require Text::ParseWords;
    return Text::ParseWords::quotewords(@_);
}

# Straight copy of parse_options() from Term::UI
sub _parse_options {
    my ($input) = @_;

    my $return = {};
    while (   $input =~ s/(?:^|\s+)--?([-\w]+=(["']).+?\2)(?=\Z|\s+)//
           or $input =~ s/(?:^|\s+)--?([-\w]+=\S+)(?=\Z|\s+)//
           or $input =~ s/(?:^|\s+)--?([-\w]+)(?=\Z|\s+)//) {
        my $match = $1;

        if ($match =~ /^([-\w]+)=(["'])(.+?)\2$/) {
            $return->{$1} = $3;

        }
        elsif ($match =~ /^([-\w]+)=(\S+)$/) {
            $return->{$1} = $2;

        }
        elsif ($match =~ /^no-?([-\w]+)$/i) {
            $return->{$1} = 0;

        }
        elsif ($match =~ /^([-\w]+)$/) {
            $return->{$1} = 1;
        }
    }

    return wantarray ? ($return, $input) : $return;
}

sub parse_options2 {
    my ($input) = @_;

    warn(colored("\n[!] Input with an odd number of quotes: <$input>", 'bold red') . "\n\n")
      if $yv_obj->get_debug;

    my ($args, $keywords) = _parse_options($input);

    my @args =
        map $args->{$_} eq '0' ? "--no-$_"
      : $args->{$_} eq '1'     ? "--$_"
      :                          "--$_=$args->{$_}" => keys %{$args};

    return wantarray ? (\@args, [split q{ }, $keywords]) : \@args;
}

sub parse_options {
    my ($input) = @_;
    my (@args, @keywords);

    if (not defined($input) or $input eq q{}) {
        return \@args, \@keywords;
    }

    foreach my $word (get_quotewords(qr/\s+/, 1, $input)) {
        if (chr ord $word eq q{-}) {
            push @args, $word;
        }
        else {
            push @keywords, $word;
        }
    }

    if (not @args and not @keywords) {
        return parse_options2($input);
    }

    return wantarray ? (\@args, \@keywords) : \@args;
}

sub get_user_input {
    my ($text) = @_;

    if (not $opt{interactive}) {
        if (not defined $opt{std_input}) {
            return ':return';
        }
    }

    my $input = unpack(
                       'A*', defined($opt{std_input})
                       ? delete($opt{std_input})
                       : ($term->readline($text) // return ':return')
                      ) =~ s/^\s+//r;

    return q{:next} if $input eq q{};    # <ENTER> for the next page

    state $x = require Encode;
    $input = Encode::decode_utf8($input);

    my ($args, $keywords) = parse_options($input);

    if ($opt{history} && @{$keywords}) {
        my $str = join(' ', grep { /\w/ and not /^[:;=]/ } @{$keywords});
        if ($str ne '' and $str !~ /^[0-9]{1,2}\z/) {
            $term->append_history(1, $opt{history_file});
        }
    }

    apply_input_arguments($args, $keywords);
    return @{$keywords};
}

sub logout {

    unlink $authentication_file
      or warn "Can't unlink: `$authentication_file' -> $!";

    $yv_obj->set_access_token();
    $yv_obj->set_refresh_token();

    return 1;
}

sub authenticate {
    my $get_code_url = $yv_obj->get_accounts_oauth_url() // return;

    print <<"INFO";

** Get the authentication code: $get_code_url

                            |
... and paste it below.    \\|/
                            `
INFO

    my $code = $term->readline(colored(q{Code: }, 'bold')) || return;

    my $info = $yv_obj->oauth_login($code) // do {
        warn "[WARNING] Can't log in... That's all I know...\n";
        return;
    };

    if (defined $info->{access_token}) {

        $yv_obj->set_access_token($info->{access_token})   // return;
        $yv_obj->set_refresh_token($info->{refresh_token}) // return;

        my $remember_me = ask_yn(prompt  => colored("\nRemember me", 'bold'),
                                 default => 'y');

        if ($remember_me) {
            $yv_obj->set_authentication_file($authentication_file);
            $yv_obj->save_authentication_tokens()
              or warn "Can't store the authentication tokens: $!";
        }
        else {
            $yv_obj->set_authentication_file();
        }

        return 1;
    }
    else {
        warn "[WARNING] There was a problem with the authentication...\n";
    }

    return;
}

sub authenticated {
    if (not defined $yv_obj->get_access_token) {
        warn_needs_auth();
        return;
    }
    return 1;
}

sub favorite_videos {
    my (@videoIDs) = @_;
    return if not authenticated();

    foreach my $id (@videoIDs) {
        my $videoID = get_valid_video_id($id) // next;

        if ($yv_obj->favorite_video($videoID)) {
            printf "\n** Video %s has been successfully favorited.\n", sprintf($CONFIG{youtube_video_url}, $videoID);
        }
        else {
            warn_cant_do('favorite', $videoID);
        }
    }
    return 1;
}

sub select_and_save_to_playlist {
    return if not authenticated();

    my $request = $yv_obj->my_playlists() // last;
    my $playlistID = print_playlists($request, return_playlist_id => 1);

    if (defined($playlistID)) {
        return save_to_playlist($playlistID, @_);
    }

    warn_no_thing_selected('playlist');
    return;

}

sub save_to_playlist {
    my ($playlistID, @videoIDs) = @_;
    return if not authenticated();

    foreach my $id (@videoIDs) {
        my $videoID = get_valid_video_id($id) // next;

        if ($yv_obj->add_video_to_playlist($playlistID, $videoID, $opt{position} || 1)) {
            printf("\n** Video %s has been successfully added to playlistID: %s\n",
                   sprintf($CONFIG{youtube_video_url}, $videoID), $playlistID);
        }
        else {
            warn_cant_do("add to playlist", $videoID);
        }
    }
    return 1;
}

sub rate_videos {
    my $rating = shift;
    return if not authenticated();

    foreach my $id (@_) {
        my $videoID = get_valid_video_id($id) // next;
        if ($yv_obj->send_rating_to_video($videoID, $rating)) {
            print "\n** VideoID '$videoID' has been successfully ${rating}d.\n";
        }
        else {
            warn colored("\n[!] VideoID '$videoID' has not been ${rating}d", 'bold red') . "\n";
        }
    }
    return 1;
}

sub get_and_play_video_ids {
    my @ids = grep { get_valid_video_id($_) } @_;

    if (not @ids) {
        warn_invalid('video IDs', "@_");
        return;
    }

    my $info = $yv_obj->video_details(join(',', @ids), EXTRA_VIDEO_PART);

    if ($yv_utils->has_entries($info)) {
        if (not play_videos($info->{results}{items})) {
            return;
        }
    }
    else {
        warn_cant_do('get info about', "@ids");
    }

    return 1;
}

sub get_and_play_playlists {
    foreach my $id (@_) {
        my $videos = $yv_obj->videos_from_playlist_id(get_valid_playlist_id($id) // next);
        local $opt{play_all} = length($opt{std_input}) ? 0 : 1;
        print_videos($videos, auto => $opt{play_all});
    }
    return 1;
}

sub get_and_print_video_info {
    foreach my $id (@_) {

        my $videoID = get_valid_video_id($id) // next;
        my $info = $yv_obj->video_details($videoID, EXTRA_VIDEO_PART);

        if ($yv_utils->has_entries($info)) {
            print_video_info($info->{results}{items}[0]);
        }
        else {
            warn_cant_get('information', $videoID);
        }
    }
    return 1;
}

sub get_and_print_related_videos {
    foreach my $id (@_) {
        my $videoID = get_valid_video_id($id) // next;
        my $results = $yv_obj->related_to_videoID($videoID);
        print_videos($results);
    }
    return 1;
}

sub get_and_print_comments {
    foreach my $id (@_) {
        my $videoID = get_valid_video_id($id) // next;
        my $comments = $yv_obj->comments_from_video_id($videoID);
        print_comments($comments, $videoID);
    }
    return 1;
}

sub get_and_print_videos_from_course {
    my ($course_id) = @_;

    if ($course_id =~ /$valid_course_id_re/) {
        my $info = $yv_obj->get_video_lectures_from_course($+{course_id});

        if ($yv_utils->has_entries($info)) {
            print_videos($info);
        }
        else {
            warn colored("\n[!] Inexistent course...", 'bold red') . "\n";
            return;
        }
    }
    else {
        warn_invalid('courseID', $course_id);
        return;
    }
    return 1;
}

sub get_and_print_clips_from_season_id {
    my ($season_id) = @_;

    if ($season_id =~ /$valid_playlist_id_re/) {
        my $info = $yv_obj->get_clips_from_season_id($season_id);

        if ($yv_utils->has_entries($info)) {
            print_videos($info);
        }
        else {
            warn colored("\n[!] No clips found...", 'bold red') . "\n";
            return;
        }
    }
    else {
        warn_invalid('seasonID', $season_id);
        return;
    }
}

sub get_and_print_episodes_from_season_id {
    my ($season_id) = @_;

    if ($season_id =~ /$valid_playlist_id_re/) {
        my $info = $yv_obj->get_episodes_from_season_id($season_id);

        if ($yv_utils->has_entries($info)) {
            print_videos($info);
        }
        else {
            warn colored("\n[!] No episode found...", 'bold red') . "\n";
            return;
        }
    }
    else {
        warn_invalid('seasonID', $season_id);
        return;
    }
}

sub get_and_print_content_from_show {
    my ($show_id) = @_;

    if ($show_id =~ /$valid_playlist_id_re/) {
        my $info = $yv_obj->get_shows_content_from_id($show_id);

        if ($yv_utils->has_entries($info)) {
            print_show_seasons($info);
        }
        else {
            warn colored("\n[!] Inexistent show...", 'bold red') . "\n";
            return;
        }
    }
    else {
        warn_invalid('showID', $show_id);
        return;
    }

    return 1;
}

sub get_and_print_videos_from_playlist {
    my ($playlistID) = @_;

    if ($playlistID =~ /$valid_playlist_id_re/) {
        my $info = $yv_obj->videos_from_playlist_id($playlistID);
        if ($yv_utils->has_entries($info)) {
            print_videos($info);
        }
        else {
            warn colored("\n[!] Inexistent playlist...", 'bold red') . "\n";
            return;
        }
    }
    else {
        warn_invalid('playlistID', $playlistID);
        return;
    }
    return 1;
}

sub subscribe_to {
    my ($is_channel, @ids) = @_;

    return if not authenticated();

    foreach my $channel (@ids) {
        if ($channel =~ /$valid_channel_id_re/) {
            if ($is_channel ? $yv_obj->subscribe_channel($channel) : $yv_obj->subscribe_channel_from_username($channel)) {
                print "** Successfully subscribed to channel: $channel\n";
            }
            else {
                warn colored("\n[!] Unable to subscribe to channel: $channel", 'bold red') . "\n";
            }
        }
    }
    return 1;
}

sub subscribe_to_channels {
    subscribe_to(1, @_);
}

sub subscribe_to_usernames {
    subscribe_to(0, @_);
}

sub _bold_color {
    my ($text) = @_;
    return colored($text, 'bold');
}

sub quit_required { $_[0] ~~ ['q', 'quit', 'exit'] }

sub youtube_urls {
    given (shift) {
        when (/$get_video_id_re/) {
            get_and_play_video_ids($+{video_id});
        }
        when (/$get_playlist_id_re/) {
            get_and_print_videos_from_playlist($+{playlist_id});
        }
        when (/$get_course_id_re/) {
            get_and_print_videos_from_course($+{course_id});
        }
        default {
            return;
        }
    }

    return 1;
}

sub general_options {
    my %args = @_;

    my $url      = $args{url};
    my $option   = $args{opt};
    my $callback = $args{sub};
    my $results  = $args{res};
    my $info     = $args{info};

    given ($option) {
        when (\&quit_required) {
            main_quit(0);
        }
        when (undef) {
            return;
        }
        when ($_ ~~ ['n', 'next'] and defined $url) {
            if (defined $info->{nextPageToken}) {
                my $request = $yv_obj->next_page($url, $info->{nextPageToken});
                $callback->($request);
            }
            else {
                warn_last_page();
            }
        }
        when ($_ ~~ ['R', 'refresh'] and defined $url) {
            @{$results} = @{$yv_obj->_get_results($url)->{results}{items}};
        }
        when ($_ ~~ ['b', 'back', 'p', 'prev', 'previous'] and defined $url) {
            if (defined $info->{prevPageToken}) {
                my $request = $yv_obj->previous_page($url, $info->{prevPageToken});
                $callback->($request);
            }
            else {
                warn_first_page();
            }
        }
        when ('login') {
            authenticate();
        }
        when ('logout') {
            logout();
        }
        when (['reset', 'reload', 'restart']) {
            @ARGV = ();
            do $0;
        }
        when (/^dv${digit_or_equal_re}(.*)/ and ref $results eq 'ARRAY') {
            if (my @nums = get_valid_numbers($#{$results}, $1)) {
                print "\n";
                foreach my $num (@nums) {
                    state $x = require Data::Dump;
                    say Data::Dump::pp($results->[$num]);
                }
                press_enter_to_continue();
            }
            else {
                warn_no_thing_selected('result');
            }
        }
        when (/^v(?:ideoids?)?=(.*)/) {
            if (my @ids = split(/[,\s]+/, $1)) {
                get_and_play_video_ids(@ids);
            }
            else {
                warn colored("\n[!] No video ID specified!", 'bold red') . "\n";
            }
        }
        when (/^playlist(?:ID)?=(.*)/) {
            get_and_print_videos_from_playlist($1);
        }
        when (/^course(?:ID)?=(.*)/) {
            get_and_print_videos_from_course($1);
        }
        when (/^courses=(.*)/) {
            print_courses($yv_obj->get_courses_from_category($1));
        }
        when (/^lec(?:tures)?=(.*)/) {
            print_videos($yv_obj->get_video_lectures_from_category($1));
        }
        default {
            return;
        }
    }

    return 1;
}

sub warn_no_results {
    warn colored("\n[!] No $_[0] results!", 'bold red') . "\n";
}

sub warn_invalid {
    my ($name, $option) = @_;
    warn colored("\n[!] Invalid $name: <$option>", 'bold red') . "\n";
}

sub warn_cant_do {
    my ($action, $videoID) = @_;
    warn colored("\n[!] Can't $action video: " . sprintf($CONFIG{youtube_video_url}, $videoID), 'bold red') . "\n";
}

sub warn_cant_get {
    my ($name, $videoID) = @_;
    warn colored("\n[!] Can't get $name for video: " . sprintf($CONFIG{youtube_video_url}, $videoID), 'bold red') . "\n";
}

sub warn_last_page {
    warn colored("\n[!] This is the last page!", "bold red") . "\n";
}

sub warn_first_page {
    warn colored("\n[!] No previous page available...", 'bold red') . "\n";
}

sub warn_no_thing_selected {
    warn colored("\n[!] No $_[0] selected!", 'bold red') . "\n";
}

sub warn_needs_auth {
    warn colored("\n[!] This function needs authentication!", 'bold red') . "\n";
}

# ... GET INPUT SUBS ... #
sub get_input_for_first_time {
    return get_user_input(_bold_color("\n=>> Search for YouTube videos (:h for help)") . "\n> ");
}

sub get_input_for_channels {
    return get_user_input(_bold_color("\n=>> Select a channel (:h for help)") . "\n> ");
}

sub get_input_for_courses {
    return get_user_input(_bold_color("\n=>> Select a course (:h for help)") . "\n> ");
}

sub get_input_for_search {
    return get_user_input(_bold_color("\n=>> Select one or more videos to play (:h for help)") . "\n> ");
}

sub get_input_for_playlists {
    return get_user_input(_bold_color("\n=>> Select a playlist (:h for help)") . "\n> ");
}

sub get_input_for_comments {
    return get_user_input(_bold_color("\n=>> Press <ENTER> for the next page of comments (:h for help)") . "\n> ");
}

sub get_input_for_shows {
    return get_user_input(_bold_color("\n=>> Select a video show (:h for help)") . "\n> ");
}

sub get_input_for_seasons {
    return get_user_input(_bold_color("\n=>> Select a show season (:h for help)") . "\n> ");
}

sub get_input_for_video_tops {
    return get_user_input(_bold_color("\n=>> Select a video top (:h for help)") . "\n> ");
}

sub get_input_for_categories {
    return get_user_input(_bold_color("\n=>> Select a category (:h for help)") . "\n> ");
}

sub ask_yn {
    my (%opt) = @_;
    my $c = join('/', map { $_ eq $opt{default} ? ucfirst($_) : $_ } qw(y n));

    my $answ;
    do {
        $answ = lc($term->readline($opt{prompt} . " [$c]: "));
        $answ = $opt{default} unless $answ =~ /\S/;
    } while ($answ !~ /^y(?:es)?$/ and $answ !~ /^no?$/);

    return chr(ord($answ)) eq 'y';
}

sub get_reply {
    my (%opt) = @_;

    my $default = 1;
    while (my ($i, $choice) = each @{$opt{choices}}) {
        print "\n" if $i == 0;
        printf("%3d> %s\n", $i + 1, $choice);
        if ($choice eq $opt{default}) {
            $default = $i + 1;
        }
    }
    print "\n";

    my $answ;
    do {
        $answ = $term->readline($opt{prompt} . " [$default]: ");
        $answ = $default unless $answ =~ /\S/;
    } while ($answ !~ /^[0-9]+\z/ or $answ < 1 or $answ > @{$opt{choices}});

    return $opt{choices}[$answ - 1];
}

sub valid_num {
    my ($num, $array_ref) = @_;
    return $num =~ /^[0-9]{1,2}\z/ && $num != 0 && $num <= @{$array_ref};
}

sub adj_width {
    my ($str, $len, $prepend) = @_;

    $len > 0 or do {
        warn "[WARN] Insufficient space for the title: increase your terminal width!\n";
        return $str;
    };

    state $pkg = (
        eval {
            require Unicode::GCString;
            'Unicode::GCString';
          } // eval {
            require Text::CharWidth;
            'Text::CharWidth';
          } // do {
            warn "[WARN] Please install Unicode::GCString or Text::CharWidth in order to use this functionality.\n";
            '';
          }
    );

    #
    ## Unicode::GCString
    #
    if ($pkg eq 'Unicode::GCString') {

        my $gcstr     = Unicode::GCString->new($str);
        my $str_width = $gcstr->columns;

        if ($str_width != $len) {
            while ($str_width > $len) {
                $gcstr = $gcstr->substr(0, -1);
                $str_width = $gcstr->columns;
            }

            $str = $gcstr->as_string;
            my $spaces = ' ' x ($len - $str_width);
            $str = $prepend ? "$spaces$str" : "$str$spaces";
        }

        return $str;
    }

    #
    ## Text::CharWidth
    #
    if ($pkg eq 'Text::CharWidth') {

        my $str_width = Text::CharWidth::mbswidth($str);

        if ($str_width != $len) {
            while ($str_width > $len) {
                chop $str;
                $str_width = Text::CharWidth::mbswidth($str);
            }

            my $spaces = ' ' x ($len - $str_width);
            $str = $prepend ? "$spaces$str" : "$str$spaces";
        }

        return $str;
    }

    return $str;
}

# ... PRINT SUBROUTINES ... #
sub print_channels {
    my ($results) = @_;

    if (not $yv_utils->has_entries($results)) {
        warn_no_results("channel");
    }

    if ($opt{get_term_width} and $opt{results_fixed_width}) {
        get_term_width();
    }

    my $url      = $results->{url};
    my $info     = $results->{results} // {};
    my $channels = $info->{items} // [];

    my $i = 0;
    foreach my $channel (@{$channels}) {

        if ($opt{results_with_details}) {
            printf(
                   "\n%s. %s\n    %s: %-23s %s: %-12s\n%s\n",
                   colored(sprintf('%2d', ++$i), 'bold') => colored($yv_utils->get_title($channel), 'bold blue'),
                   colored('Updated' => 'bold') => $yv_utils->get_publication_date($channel),
                   colored('Author'  => 'bold') => $yv_utils->get_channel_title($channel),
                   wrap_text(
                             i_tab => q{ } x 4,
                             s_tab => q{ } x 4,
                             text  => [$yv_utils->get_description($channel) || 'No description available...']
                            ),
                  );
        }
        elsif ($opt{results_fixed_width}) {

            state $x = require List::Util;

            my @authors = map { $yv_utils->get_channel_title($_) } @{$channels};
            my @dates   = map { $yv_utils->get_publication_date($_) } @{$channels};

            my $author_width = List::Util::min(List::Util::max(map { length($_) } @authors), int($term_width / 5));
            my $dates_width = List::Util::max(map { length($_) } @dates);
            my $title_length = $term_width - ($author_width + $dates_width + 2 + 3 + 1 + 2);

            print "\n";
            foreach my $i (0 .. $#{$channels}) {
                my $channel = $channels->[$i];
                printf "%s. %s %s [%*s]\n", colored(sprintf('%2d', $i + 1), 'bold'),
                  adj_width($yv_utils->get_title($channel), $title_length),
                  adj_width($authors[$i], $author_width, 1),
                  $dates_width, $dates[$i];
            }
            last;
        }
        else {
            print "\n" if $i == 0;
            printf "%s. %s (by %s)\n", colored(sprintf('%2d', ++$i), 'bold'), $yv_utils->get_title($channel),
              $yv_utils->get_channel_title($channel);
        }
    }

    my @keywords = get_input_for_channels();

    my @for_search;
    foreach my $key (@keywords) {
        if ($key =~ /$valid_opt_re/) {
            given ($1) {
                when (
                      general_options(
                                      opt  => $_,
                                      sub  => __SUB__,
                                      url  => $url,
                                      res  => $channels,
                                      info => $info,
                                     )
                  ) {
                }
                when (['h', 'help']) {
                    print $general_help;
                    press_enter_to_continue();
                }
                when (['r', 'return']) {
                    return;
                }
                default {
                    warn_invalid('option', $_);
                }
            }
        }
        else {
            given ($key) {
                when (\&youtube_urls) { }    # do nothing
                when (valid_num($_, $channels)) {
                    print_videos($yv_obj->uploads($yv_utils->get_channel_id($channels->[$_ - 1])));
                }
                default {
                    push @for_search, $_;
                }
            }
        }
    }

    if (@for_search) {
        __SUB__->($yv_obj->search_channels(@for_search));
    }

    __SUB__->(@_);
}

sub print_show_seasons {
    my ($results) = @_;

    ...;    # NEEDS WORK!!!

    if (not @{$results->{results}}) {
        warn colored("\n[!] No seasons found...", 'bold red') . "\n";
    }

    my $url     = $results->{url};
    my $seasons = $results->{results};

    my $i = 0;
    foreach my $season (@{$seasons}) {
        if ($season->{title} =~ /^([0-9]+)$/) {
            $season->{title} = "Season $1";
        }

        printf(
               "\n%s. %s\n    %s: %-13s %-13s: %-20s %s: %s\n" . "    %s: %-12s %s: %s\n",
               colored(sprintf('%2d', ++$i), 'bold') => colored($season->{title}, 'bold blue'),
               colored('Episodes'  => 'bold') => $yv_utils->set_thousands($season->{episodes}),
               colored('Clips'     => 'bold') => $yv_utils->set_thousands($season->{clips}),
               colored('Author'    => 'bold') => $season->{name},
               colored('Published' => 'bold') => $yv_utils->format_date($season->{published}),
               colored('Updated '  => 'bold') => $yv_utils->format_date($season->{updated}),
              );
    }

    my @keywords = get_input_for_seasons();

    foreach my $key (@keywords) {
        if ($key =~ /$valid_opt_re/) {
            given ($1) {
                when (
                      general_options(
                                      opt  => $_,
                                      sub  => __SUB__,
                                      url  => $url,
                                      res  => $seasons,
                                      mode => 'shows_content',
                                     )
                  ) {
                }
                when (['r', 'return']) {
                    return;
                }
                when (['h', 'help']) {
                    print $general_help;
                    press_enter_to_continue();
                }
            }
        }
        else {
            given ($key) {
                when (\&youtube_urls) { }    # do nothing
                when (valid_num($_, $seasons)) {

                    if ($seasons->[$_ - 1]{episodes} > 0 and $seasons->[$_ - 1]{clips} > 0) {
                        my $reply = get_reply(
                                              prompt  => colored('=>> Episodes or clips?', 'bold'),
                                              choices => [qw(episodes clips)],
                                              default => 'episodes',
                                             );

                        if ($reply eq 'episodes') {
                            get_and_print_episodes_from_season_id($seasons->[$_ - 1]{seasonID});
                        }
                        else {
                            get_and_print_clips_from_season_id($seasons->[$_ - 1]{seasonID});
                        }
                    }
                    elsif ($seasons->[$_ - 1]{episodes} > 0) {
                        get_and_print_episodes_from_season_id($seasons->[$_ - 1]{seasonID});

                    }
                    elsif ($seasons->[$_ - 1]{clips} > 0) {
                        get_and_print_clips_from_season_id($seasons->[$_ - 1]{seasonID});
                    }
                    else {
                        warn colored("\n[!] This show contains 0 episodes and 0 clips!", 'bold red') . "\n";
                    }
                }
                default {
                    warn_invalid('keyword', $_);
                }
            }
        }
    }

    __SUB__->(@_);
}

sub print_shows {
    my ($results) = @_;

    ...;    # NEEDS WORK!!!

    if (not @{$results->{results}}) {
        warn colored("\n[!] No shows found...", 'bold red') . "\n";
    }

    my $url   = $results->{url};
    my $shows = $results->{results};

    my $i = 0;
    foreach my $show (@{$shows}) {
        if ($opt{results_with_details}) {
            printf(
                   "\n%s. %s [%s]\n%s\n",
                   colored(sprintf('%2d', ++$i), 'bold') => colored($show->{title}, 'bold blue'),
                   colored('seasons: ' . sprintf('%d', $show->{seasons}), 'bold'),
                   wrap_text(
                             i_tab => q{ } x 4,
                             s_tab => q{ } x 4,
                             text  => [$show->{summary} || 'No description available...'],
                            ),
                  );
        }
        elsif ($opt{results_fixed_width}) {

            state $x = require List::Util;

            my $max_author_len = List::Util::min(List::Util::max(map { length($_->{name}) } @{$shows}), int($term_width / 5));
            my $count_width = List::Util::max(map { length($_->{seasons}) } @{$shows});
            my $title_length = $term_width - ($max_author_len + $count_width + 2 + 3 + 1 + 2);

            foreach my $show (@{$shows}) {
                print "\n" if $i == 0;
                printf "%s. %s %s [%*s]\n", colored(sprintf('%2d', ++$i), 'bold'),
                  adj_width($show->{title}, $title_length),
                  adj_width($show->{name}, $max_author_len, 1),
                  $count_width, $show->{seasons};
            }
            last;
        }
        else {
            print "\n" if $i == 0;
            printf "%s. %s (by %s) [%d]\n", colored(sprintf('%2d', ++$i), 'bold'), $show->{title}, $show->{name},
              $show->{seasons};
        }
    }

    my @keywords = get_input_for_shows();

    foreach my $key (@keywords) {
        if ($key =~ /$valid_opt_re/) {
            given ($1) {
                when (
                      general_options(
                                      opt  => $_,
                                      sub  => __SUB__,
                                      url  => $url,
                                      res  => $shows,
                                      mode => 'shows',
                                     )
                  ) {
                }
                when (['r', 'return']) {
                    return;
                }
                when (['h', 'help']) {
                    print $general_help;
                    press_enter_to_continue();
                }
                default {
                    warn_invalid('option', $_);
                }
            }
        }
        else {
            given ($key) {
                when (\&youtube_urls) { }    # do nothing
                when (valid_num($_, $shows)) {
                    get_and_print_content_from_show($shows->[$_ - 1]{showID});
                }
                default {
                    warn_invalid('keyword', $_);
                }
            }
        }
    }

    __SUB__->(@_);
}

sub print_comments {
    my ($results, $videoID) = @_;

    if (not $yv_utils->has_entries($results)) {
        warn_no_results("comments");
    }

    my $url      = $results->{url};
    my $info     = $results->{results} // {};
    my $comments = $info->{items} // [];

    my $i = 0;
    foreach my $comment (@{$comments}) {
        my $snippet = (($comment->{snippet} // next)->{topLevelComment} // next)->{snippet};

        printf(
               "\n%s on %s said:\n%s\n",
               colored($snippet->{authorDisplayName}, 'bold'),
               $yv_utils->format_date($snippet->{publishedAt}),
               wrap_text(
                         i_tab => q{ } x 4,
                         s_tab => q{ } x 4,
                         text  => [$snippet->{textDisplay} // 'Empty comment...']
                        ),
              );
    }

    my @keywords = get_input_for_comments();

    foreach my $key (@keywords) {
        if ($key =~ /$valid_opt_re/) {
            given ($1) {
                when (
                      general_options(
                                      opt  => $_,
                                      sub  => __SUB__,
                                      url  => $url,
                                      res  => $comments,
                                      info => $info,
                                      mode => 'comments',
                                      args => [$videoID],
                                     )
                  ) {
                }
                when (['h', 'help']) {
                    print $comments_help;
                    press_enter_to_continue();
                }
                when (['c', 'comment']) {
                    if (authenticated()) {
                        require File::Temp;
                        my ($fh, $filename) = File::Temp::tempfile();
                        $yv_obj->proxy_system($ENV{EDITOR} // 'nano', $filename);
                        if ($?) {
                            warn colored("\n[!] Editor exited with a non-zero code. Unable to continue!", 'bold red') . "\n";
                        }
                        else {
                            my $comment = do { local (@ARGV, $/) = $filename; <> };
                            $comment =~ s/[^\s[:^cntrl:]]+//g;    # remove control characters

                            if (length($comment) and $yv_obj->comment_to_video_id($comment, $videoID)) {
                                print "\n** Comment posted!\n";
                            }
                            else {
                                warn colored("\n[!] Your comment has NOT been posted!", 'bold red') . "\n";
                            }
                        }
                    }
                }
                when (['r', 'return']) {
                    return;
                }
                default {
                    warn_invalid('option', $_);
                }
            }
        }
        else {
            given ($key) {
                when (\&youtube_urls) { }    # do nothing
                when (valid_num($_, $comments)) {
                    print_videos($yv_obj->get_videos_from_username($comments->[$_ - 1]{author}));
                }
                default {
                    warn_invalid('keyword', $_);
                }
            }
        }
    }

    __SUB__->(@_);
}

sub print_courses {
    my ($results) = @_;

    ...;    # NEEDS WORK!!!

    my $url     = $results->{url};
    my $courses = $results->{results};

    my $i = 0;
    foreach my $course (@{$courses}) {
        if ($opt{results_with_details}) {
            printf(
                   "\n%s. %s\n%s\n",
                   colored(sprintf('%2d', ++$i), 'bold') => colored($course->{title}, 'bold blue'),
                   wrap_text(
                             i_tab => q{ } x 4,
                             s_tab => q{ } x 4,
                             text  => [$course->{summary} || 'No description available...'],
                            ),
                  );
        }
        else {
            print "\n" if $i == 0;
            printf "%s. %s (%s)\n", colored(sprintf('%2d', ++$i), 'bold'), $course->{title},
              $yv_utils->format_date($course->{updated});
        }
    }

    my @keywords = get_input_for_courses();

    foreach my $key (@keywords) {
        if ($key =~ /$valid_opt_re/) {
            given ($1) {
                when (
                      general_options(
                                      opt  => $_,
                                      sub  => __SUB__,
                                      url  => $url,
                                      res  => $courses,
                                      mode => 'courses',
                                     )
                  ) {
                }
                when (['h', 'help']) {
                    print $general_help;
                    press_enter_to_continue();
                }
                when (['r', 'return']) {
                    return;
                }
                default {
                    warn_invalid('option', $_);
                }
            }
        }
        else {
            given ($key) {
                when (\&youtube_urls) { }    # do nothing
                when (valid_num($_, $courses)) {
                    get_and_print_videos_from_course($courses->[$_ - 1]{courseID});
                }
                default {
                    warn_invalid('keyword', $_);
                }
            }
        }
    }

    __SUB__->(@_);
}

sub print_categories {
    my ($results) = @_;

    my $categories = $results->{items};
    return if ref $categories ne 'ARRAY';

    my $i = 0;
    print "\n" if @{$categories};

    foreach my $category (@{$categories}) {

        # Ignore nonassignable categories
        $category->{snippet}{assignable} || next;

        printf "%s. %-40s (id: %s)\n", colored(sprintf('%2d', ++$i), 'bold'), $yv_utils->get_title($category), $category->{id};
    }

    my @keywords = get_input_for_categories();

    foreach my $key (@keywords) {
        if ($key =~ /$valid_opt_re/) {
            given ($1) {
                when (
                      general_options(
                                      opt => $_,
                                      sub => __SUB__,
                                      res => $results,
                                     )
                  ) {
                }
                when (['h', 'help']) {
                    print $general_help;
                    press_enter_to_continue();
                }
                when (['r', 'return']) {
                    return;
                }
                default {
                    warn_invalid('option', $_);
                }
            }
        }
        else {
            given ($key) {
                when (\&youtube_urls) { }    # do nothing
                when (valid_num($_, $categories)) {
                    my $cat_id = $categories->[$_ - 1]{id};
                    print_videos($yv_obj->videos_from_category($cat_id));
                }
                default {
                    warn_invalid('keyword', $_);
                }
            }
        }
    }

    __SUB__->(@_);
}

sub print_movies {
    my $i = 0;

    print "\n";
    foreach my $id (@{WWW::YoutubeViewer::movie_IDs}) {
        my $top_movie_name = uc $id;
        $top_movie_name =~ tr/_/ /;
        printf "%s. %s\n", colored(sprintf('%2d', ++$i), 'bold'), $top_movie_name;
    }

    my @keywords = get_input_for_video_tops();

    foreach my $key (@keywords) {
        if ($key =~ /$valid_opt_re/) {
            given ($1) {
                when (general_options(opt => $_)) { }
                when (['h', 'help']) {
                    print $general_help;
                    press_enter_to_continue();
                }
                when (['r', 'return']) {
                    return;
                }
                default {
                    warn_invalid('option', $_);
                }
            }
        }
        else {
            given ($key) {
                when (\&youtube_urls) { }    # do nothing
                when (valid_num($_, \@{WWW::YoutubeViewer::movie_IDs})) {
                    print_videos($yv_obj->get_movies(${WWW::YoutubeViewer::movie_IDs}[$_ - 1]));
                }
                default {
                    warn_invalid('keyword', $_);
                }
            }
        }
    }

    __SUB__->();
}

sub print_video_tops {
    my (%top_opts) = @_;

    ...;    # NEEDS WORK!!!

    print "\n";
    my $i = 0;
    foreach my $id (@{WWW::YoutubeViewer::feeds_IDs}) {
        my $top_name = uc $id;
        $top_name =~ tr/_/ /;
        printf "%s. %s\n", colored(sprintf('%2d', ++$i), 'bold'), $top_name;
    }

    my @keywords = get_input_for_video_tops();

    foreach my $key (@keywords) {
        if ($key =~ /$valid_opt_re/) {
            given ($1) {
                when (general_options(opt => $_)) { }
                when (['h', 'help']) {
                    print $general_help;
                    press_enter_to_continue();
                }
                when (['r', 'return']) {
                    return;
                }
                default {
                    warn_invalid('option', $_);
                }
            }
        }
        else {
            given ($key) {
                when (\&youtube_urls) { }    # do nothing
                when (valid_num($_, \@{WWW::YoutubeViewer::feeds_IDs})) {
                    $top_opts{feed_id} = ${WWW::YoutubeViewer::feeds_IDs}[$_ - 1];
                    undef $top_opts{time_id} if $_ ~~ [3, 5, 8, 9];    # doesn't support the 'time' option
                    print_videos($yv_obj->get_video_tops(%top_opts));
                }
                default {
                    warn_invalid('keyword', $_);
                }
            }
        }
    }

    __SUB__->(@_);
}

sub print_playlists {
    my ($results, %args) = @_;

    if (not $yv_utils->has_entries($results)) {
        warn_no_results("playlist");
    }

    if ($opt{get_term_width} and $opt{results_fixed_width}) {
        get_term_width();
    }

    my $url       = $results->{url};
    my $info      = $results->{results} // {};
    my $playlists = $info->{items} // [];

    if ($opt{shuffle}) {
        state $x = require List::Util;
        $playlists = [List::Util::shuffle(@{$playlists})];
    }

    state $info_format = <<"FORMAT";

TITLE: %s
   ID: %s
  URL: https://www.youtube.com/playlist?list=%s
DESCR: %s
FORMAT

    foreach my $i (0 .. $#{$playlists}) {
        my $playlist = $playlists->[$i];
        if ($opt{results_with_details}) {
            printf(
                   "\n%s. %s\n    %s: %-25s %s: %s\n%s\n",
                   colored(sprintf('%2d', $i + 1), 'bold') => colored($yv_utils->get_title($playlist), 'bold blue'),
                   colored('Updated' => 'bold') => $yv_utils->get_publication_date($playlist),
                   colored('Author'  => 'bold') => $yv_utils->get_channel_title($playlist),
                   wrap_text(
                             i_tab => q{ } x 4,
                             s_tab => q{ } x 4,
                             text  => [$yv_utils->get_description($playlist) || 'No description available...']
                            ),
                  );
        }
        elsif ($opt{results_fixed_width}) {

            state $x = require List::Util;

            my @authors = map { $yv_utils->get_channel_title($_) } @{$playlists};
            my @dates   = map { $yv_utils->get_publication_date($_) } @{$playlists};

            my $author_width = List::Util::min(List::Util::max(map { length($_) } @authors), int($term_width / 5));
            my $dates_width = List::Util::max(map { length($_) } @dates);
            my $title_length = $term_width - ($author_width + $dates_width + 2 + 3 + 1 + 2);

            print "\n";
            foreach my $i (0 .. $#{$playlists}) {
                my $playlist = $playlists->[$i];
                printf "%s. %s %s [%*s]\n", colored(sprintf('%2d', $i + 1), 'bold'),
                  adj_width($yv_utils->get_title($playlist), $title_length),
                  adj_width($authors[$i], $author_width, 1),
                  $dates_width, $dates[$i];
            }
            last;
        }
        elsif ($opt{results_with_colors}) {
            print "\n" if $i == 0;
            printf(
                   "%s. %s (%s) [%s]\n",
                   colored(sprintf('%2d', $i + 1), 'bold'),
                   colored($yv_utils->get_title($playlist),                 'bold green'),
                   colored("by " . $yv_utils->get_channel_title($playlist), 'bold yellow'),
                   colored($yv_utils->get_publication_date($playlist),      'bold blue'),
                  );
        }
        else {
            print "\n" if $i == 0;
            printf(
                   "%s. %s (by %s) [%s]\n",
                   colored(sprintf('%2d', $i + 1), 'bold'), $yv_utils->get_title($playlist),
                   $yv_utils->get_channel_title($playlist), $yv_utils->get_publication_date($playlist)
                  );
        }
    }

    state @keywords;
    if ($args{auto}) { }    # do nothing...
    else {
        @keywords = get_input_for_playlists();
        if (scalar(@keywords) == 0) {
            __SUB__->(@_);
        }
    }

    my $contains_keywords = grep /$non_digit_or_opt_re/, @keywords;

    my @for_search;
    foreach my $key (@keywords) {
        if ($key =~ /$valid_opt_re/) {
            given ($1) {
                when (
                      general_options(
                                      opt  => $_,
                                      sub  => __SUB__,
                                      url  => $url,
                                      res  => $playlists,
                                      info => $info,
                                      mode => 'playlists',
                                     )
                  ) {
                }
                when (['h', 'help']) {
                    print $playlists_help;
                    press_enter_to_continue();
                }
                when (['r', 'return']) {
                    return;
                }
                when (/^i(?:nfo)?${digit_or_equal_re}(.*)/) {
                    if (my @ids = get_valid_numbers($#{$playlists}, $1)) {
                        foreach my $id (@ids) {
                            my $desc = wrap_text(
                                       i_tab => q{ } x 7,
                                       s_tab => q{ } x 7,
                                       text => [$yv_utils->get_description($playlists->[$id]) || 'No description available...']
                            );
                            $desc =~ s/^\s+//;
                            printf $info_format, $yv_utils->get_title($playlists->[$id]),
                              ($yv_utils->get_playlist_id($playlists->[$id])) x 2, $desc;
                        }
                        press_enter_to_continue();
                    }
                    else {
                        warn_no_thing_selected('playlist');
                    }
                }
                when (/^pp${digit_or_equal_re}(.*)/) {
                    if (my @ids = get_valid_numbers($#{$playlists}, $1)) {
                        my $arg = "--pp=" . join(q{,}, map { $yv_utils->get_playlist_id($_) } @{$playlists}[@ids]);
                        apply_input_arguments([$arg]);
                    }
                    else {
                        warn_no_thing_selected('playlist');
                    }
                }
                default {
                    warn_invalid('option', $_);
                }
            }
        }
        else {
            given ($key) {
                when (\&youtube_urls) { }    # do nothing
                when (valid_num($_, $playlists) and not $contains_keywords) {
                    if ($args{return_playlist_id}) {
                        return $yv_utils->get_playlist_id($playlists->[$_ - 1]);
                    }
                    get_and_print_videos_from_playlist($yv_utils->get_playlist_id($playlists->[$_ - 1]));
                }
                default {
                    push @for_search, $_;
                }
            }
        }
    }

    if (@for_search) {
        __SUB__->($yv_obj->search_playlists(\@for_search));
    }

    __SUB__->(@_);
}

sub compile_regex {
    my ($value) = @_;
    $value =~ s{^(?<quote>['"])(?<regex>.+)\g{quote}$}{$+{regex}}s;

    my $re = eval { use re qw(eval); qr/$value/i };

    if ($@) {
        warn_invalid("regex", $@);
        return;
    }

    return $re;
}

sub get_range_numbers {
    my ($first, $second) = @_;

    return (
            $first > $second
            ? (reverse($second .. $first))
            : ($first .. $second)
           );
}

sub get_valid_numbers {
    my ($max, $input) = @_;

    my @output;
    foreach my $id (split(/[,\s]+/, $input)) {
        push @output,
            $id =~ /$range_num_re/ ? get_range_numbers($1, $2)
          : $id =~ /^[0-9]{1,2}\z/ ? $id
          :                          next;
    }

    return grep { $_ >= 0 and $_ <= $max } map { $_ - 1 } @output;
}

sub get_streaming_url {
    my ($video_id) = @_;

    my @info = $yv_obj->get_streaming_urls($video_id);

    my (@urls, @captions);
    foreach my $entry (@info) {
        if (exists $entry->{url} and exists $entry->{itag}) {
            push @urls, $entry;
        }
        elsif (exists $entry->{lc} and exists $entry->{u}) {
            push @captions, $entry;
        }
    }

    # Download the closed-captions
    my $srt_file;
    if (@captions and $opt{get_captions} and not $opt{novideo}) {
        state $x = require WWW::YoutubeViewer::GetCaption;
        my $yv_cap = WWW::YoutubeViewer::GetCaption->new(
                                                         auto_captions => $opt{auto_captions},
                                                         captions_dir  => $opt{captions_dir},
                                                         captions      => \@captions,
                                                         languages     => $CONFIG{srt_languages},
                                                        );
        $srt_file = $yv_cap->save_caption($video_id);
    }

    state $x        = require WWW::YoutubeViewer::Itags;
    state $yv_itags = WWW::YoutubeViewer::Itags->new();

    # Include DASH itags
    my $dash = 1;

    # Exclude DASH itags in download-mode or when no video output is required
    if ($opt{download_video} or $opt{novideo} or not $opt{dash_support}) {
        $dash = 0;
    }

    my ($streaming, $resolution) =
      $yv_itags->find_streaming_url(
                                    urls           => \@urls,
                                    resolution     => $opt{resolution},
                                    dash           => $dash,
                                    dash_mp4_audio => $opt{dash_mp4_audio},
                                   );

    my $info = {};
    if (not @urls) {
        %{$info} = map { ref($_) eq 'HASH' ? %{$_} : () } @info;
    }

    return {
            streaming  => $streaming,
            srt_file   => $srt_file,
            info       => $info,
            resolution => $resolution,
           };
}

sub download_video {
    my ($streaming, $info) = @_;

    my $fat32safe = $opt{fat32safe};
    state $unix_like = $^O ~~ [qw(linux freebsd openbsd)];

    if (not $fat32safe and not $unix_like) {
        $fat32safe = 1;
    }

    my $video_filename = $yv_utils->format_text(
                                                streaming => $streaming,
                                                info      => $info,
                                                text      => $opt{video_filename_format},
                                                escape    => 0,
                                                fat32safe => $fat32safe,
                                               );

    my $naked_filename = $video_filename =~ s/\.\w+\z//r;

    if (not -d $opt{downloads_dir}) {
        state $x = require File::Path;
        unless (File::Path::make_path($opt{downloads_dir})) {
            warn colored("\n[!] Can't create directory '$opt{downloads_dir}': $1", 'bold red') . "\n";
        }
    }

    if (not -w $opt{downloads_dir}) {
        warn colored("\n[!] Can't write into directory '$opt{downloads_dir}': $!", 'bold red') . "\n";
        $opt{downloads_dir} = -w curdir() ? curdir() : return;
    }

    $video_filename = catfile($opt{downloads_dir}, $video_filename);

    if ($opt{skip_if_exists} and -e $video_filename) {
        print "** Video '$video_filename' already exists. Skipping...\n";
    }
    else {
        my $i = 0;
        while (-e $video_filename and not $opt{clobber} and ++$i) {
            my $last_i = $i > 1 ? $i - 1 : q{/};
            $video_filename =~ s{(?:_$last_i)?(\.\w{3,4})$}{_$i$1};
        }

        # Download video with wget
        if ($opt{download_with_wget}) {
            my @cmd = ("wget", ($opt{clobber} ? () : q{-nc}), $streaming->{streaming}{url}, q{-O}, $video_filename);

            if ($opt{download_in_parallel}) {
                my $pid = fork() // warn "[ERROR] Can't fork: $!";
                if ($pid == 0) {
                    $yv_obj->proxy_exec(@cmd, '--quiet');
                }
            }
            else {
                $yv_obj->proxy_system(@cmd);
                return if $?;
            }
        }

        # Download video with LWP::UserAgent
        else {

            # Show progress while downloading
            if (not $yv_obj->get_debug) {
                $yv_obj->{lwp}->show_progress(1);
            }

            if ($opt{download_in_parallel}) {

                # Don't show the progress while in parallel
                if (not $yv_obj->get_debug) {
                    $yv_obj->{lwp}->show_progress(0);
                }

                my $pid = fork() // warn "[ERROR] Can't fork: $!";
                if ($pid == 0) {
                    $yv_obj->lwp_mirror($streaming->{streaming}{url}, $video_filename);
                    exit;
                }
            }
            else {
                $yv_obj->lwp_mirror($streaming->{streaming}{url}, $video_filename);
            }

            # No progress afterwards
            if (not $yv_obj->get_debug) {
                $yv_obj->{lwp}->show_progress(0);
            }
        }
    }

    # Convert the downloaded video
    if (defined $opt{convert_to}) {
        my $convert_filename = "$naked_filename.$opt{convert_to}";
        my $convert_cmd      = $opt{convert_cmd};

        my %table = (
                     'IN'  => $video_filename,
                     'OUT' => $convert_filename,
                    );

        my $regex = do {
            local $" = '|';
            qr/\*(@{[keys %table]})\*/;
        };

        $convert_cmd =~ s/$regex/\Q$table{$1}\E/g;
        say $convert_cmd if $yv_obj->get_debug;

        $yv_obj->proxy_system($convert_cmd);

        if ($? == 0 and not $opt{keep_original_video}) {
            unlink $video_filename
              or warn colored("\n[!] Can't unlink file '$video_filename': $!", 'bold red') . "\n\n";
        }
    }

    # Play the download video
    elsif ($opt{download_and_play}) {

        my $command = get_player_command($streaming, $info);
        say $command if $yv_obj->get_debug;

        $yv_obj->proxy_system(join(q{ }, $command, quotemeta($video_filename)));

        # Remove it afterwards
        if ($? == 0 and $opt{remove_played_file}) {
            unlink $video_filename
              or warn colored("\n[!] Can't unlink file '$video_filename': $!", 'bold red') . "\n\n";
        }
    }

    # Copy the .srt file from captions dir to downloads dir
    if ($opt{copy_caption} and -e $video_filename and defined($streaming->{srt_file})) {
        my $from = $streaming->{srt_file};
        my $to = catfile($opt{downloads_dir}, "$naked_filename.srt");

        state $x = require File::Copy;
        File::Copy::cp($from, $to);
    }

    return 1;
}

sub get_player_command {
    my ($streaming, $video) = @_;

    $MPLAYER{fullscreen} = $opt{fullscreen} ? $opt{video_players}{$opt{video_player_selected}}{fs}      // '' : q{};
    $MPLAYER{novideo}    = $opt{novideo}    ? $opt{video_players}{$opt{video_player_selected}}{novideo} // '' : q{};
    $MPLAYER{mplayer_arguments} = $opt{video_players}{$opt{video_player_selected}}{arg} // q{};

    my $cmd = join(
        q{ },
        (
            # Video player
            $opt{video_players}{$opt{video_player_selected}}{cmd},

            (    # Audio file (https://)
               ref($streaming->{streaming}{__AUDIO__}) eq 'HASH'
                 && exists($opt{video_players}{$opt{video_player_selected}}{audio})
               ? $opt{video_players}{$opt{video_player_selected}}{audio}
               : ()
            ),

            (    # Caption file (.srt)
               defined($streaming->{srt_file})
                 && exists($opt{video_players}{$opt{video_player_selected}}{srt})
               ? $opt{video_players}{$opt{video_player_selected}}{srt}
               : ()
            ),

            # Rest of the arguments
            grep({ defined($_) and /\S/ } values %MPLAYER)
        )
    );

    my $has_video = $cmd =~ /\*(?:VIDEO|URL|ID)\*/;

    $cmd = $yv_utils->format_text(
                                  streaming => $streaming,
                                  info      => $video,
                                  text      => $cmd,
                                  escape    => 1,
                                 );

    $has_video ? $cmd : join(' ', $cmd, quotemeta($streaming->{streaming}{url}));
}

sub play_videos {
    my ($videos) = @_;

    my @streaming_urls;
    foreach my $video (@{$videos}) {

        my $video_id = $yv_utils->get_video_id($video);

        # It may be downloaded, but that's OK...
        if ($opt{highlight_watched}) {
            $watched_videos{$video_id} = 1;
        }

        if (defined($opt{max_seconds}) and $opt{max_seconds} >= 0) {
            next if $yv_utils->get_duration($video) > $opt{max_seconds};
        }

        if (defined($opt{min_seconds}) and $opt{min_seconds} >= 0) {
            next if $yv_utils->get_duration($video) < $opt{min_seconds};
        }

        my $streaming = get_streaming_url($video_id);

        if (defined $streaming->{info}{status} and $streaming->{info}{status} =~ /\bfail/i) {
            warn colored("[x_x] Error on: ", 'bold red') . sprintf($CONFIG{youtube_video_url}, $video_id) . "\n";
            warn colored("[x_x] Reason: ", 'bold red') . $streaming->{info}{reason} =~ s/\+/ /gr . "\n\n";
        }

        if (ref($streaming->{streaming}) ne 'HASH') {
            warn colored("[x_x] No streaming URL has been found...", 'bold red') . "\n";
            next;
        }

        # Dump metadata information
        if (defined($opt{dump})) {

            my $file = $video_id . '.' . $opt{dump};
            open(my $fh, '>:utf8', $file)
              or die "Can't open file `$file' for writing: $!";

            local $video->{streaming} = $streaming;

            if ($opt{dump} eq 'json') {
                print {$fh} JSON->new->pretty(1)->encode($video);
            }
            elsif ($opt{dump} eq 'perl') {
                require Data::Dump;
                print {$fh} Data::Dump::pp($video);
            }

            close $fh;
        }

        if ($opt{download_video}) {
            print_video_info($video);
            if (not download_video($streaming, $video)) {
                return;
            }
        }
        elsif (length($opt{extract_info})) {
            my $fh = $opt{extract_info_fh} // \*STDOUT;
            say {$fh}
              $yv_utils->format_text(
                                     streaming => $streaming,
                                     info      => $video,
                                     text      => $opt{extract_info},
                                     escape    => $opt{escape_info},
                                     fat32safe => $opt{fat32safe},
                                    );
        }
        elsif ($opt{combine_multiple_videos}) {
            print_video_info($video);
            push @streaming_urls, $streaming;
        }
        else {
            print_video_info($video);
            my $command = get_player_command($streaming, $video);

            if ($yv_obj->get_debug) {
                say "-> Resolution: $streaming->{resolution}";
                say "-> Video itag: $streaming->{streaming}{itag}";
                say "-> Audio itag: $streaming->{streaming}{__AUDIO__}{itag}" if exists $streaming->{streaming}{__AUDIO__};
                say "-> Video type: $streaming->{streaming}{type}";
                say "-> Audio type: $streaming->{streaming}{__AUDIO__}{type}" if exists $streaming->{streaming}{__AUDIO__};
                say "-> Command: $command";
            }

            $yv_obj->proxy_system($command);    # execute the video player
            if ($? and $? != 512) {
                $opt{auto_next_page} = 0;
                return;
            }
        }

        press_enter_to_continue() if $opt{confirm};
    }

    if ($opt{combine_multiple_videos} && @streaming_urls) {
        my $streaming = $streaming_urls[0];

        my $command = get_player_command($streaming, $videos->[0]);
        say $command if $yv_obj->get_debug;

        $yv_obj->proxy_system(join(q{ }, $command, map { quotemeta($_->{streaming}{url}) } @streaming_urls));
        return if $?;
    }

    return 1;
}

sub play_videos_matched_by_regex {
    my %args = @_;

    my $key    = $args{key};
    my $regex  = $args{regex};
    my $videos = $args{videos};

    my $sub = \&{'WWW::YoutubeViewer::Utils' . '::' . 'get_' . $key};

    if (not defined &$sub) {
        warn colored("\n[!] Invalid key: <$key>.", 'bold red') . "\n";
        return;
    }

    if (defined(my $re = compile_regex($regex))) {
        if (my @nums = grep { $yv_utils->$sub($videos->[$_]) =~ /$re/ } 0 .. $#{$videos}) {
            if (not play_videos([@{$videos}[@nums]])) {
                return;
            }
        }
        else {
            warn colored("\n[!] No video <$key> matched by the regex: $re", 'bold red') . "\n";
            return;
        }
    }

    return 1;
}

sub print_video_info {
    my ($video) = @_;

    my $hr = '-' x ($opt{get_term_width} ? get_term_width() : $term_width);

    printf(
           "\n%s %s\n%s\n%s\n%s\n%s",
           _bold_color('=>>'),
           'Description',
           $hr,
           wrap_text(
                     i_tab => q{},
                     s_tab => q{},
                     text  => [$yv_utils->get_description($video) || 'No description available...']
                    ),
           $hr,
           _bold_color('* URL: ')
          );

    print STDOUT sprintf($CONFIG{youtube_video_url}, $yv_utils->get_video_id($video));

    my $title        = $yv_utils->get_title($video);
    my $title_length = length($title);
    my $rep          = ($term_width - $title_length) / 2 - 4;

    $rep = 0 if $rep < 0;

    print "\n$hr\n", q{ } x $rep => (_bold_color("=>> $title <<=") . "\n\n"),
      map(sprintf(q{-> } . "%-*s: %s\n", $opt{_colors} ? 18 : 10, _bold_color($_->[0]), $_->[1]),
          (
           ['Channel'    => $yv_utils->get_channel_title($video)],
           ['ChannelID'  => $yv_utils->get_channel_id($video)],
           ['Definition' => $yv_utils->get_definition($video)],
           ['Duration'   => $yv_utils->format_time($yv_utils->get_duration($video))],
           ['Likes'      => $yv_utils->set_thousands($yv_utils->get_likes($video))],
           ['Dislikes'   => $yv_utils->set_thousands($yv_utils->get_dislikes($video))],
           ['Comments'   => $yv_utils->set_thousands($yv_utils->get_comments($video))],
           ['Views'      => $yv_utils->set_thousands($yv_utils->get_views($video))],
           ['Published'  => $yv_utils->get_publication_date($video)],
            )),
      "$hr\n";

    return 1;
}

sub print_videos {
    my ($results, %args) = @_;

    if (not $yv_utils->has_entries($results)) {
        warn_no_results("video");
    }

    if ($opt{get_term_width} and $opt{results_fixed_width}) {
        get_term_width();
    }

    my $url    = $results->{url};
    my $info   = $results->{results} // {};
    my $videos = $info->{items} // [];

    if ($opt{shuffle}) {
        state $x = require List::Util;
        $videos = [List::Util::shuffle(@{$videos})];
    }

    if (@{$videos} and not $results->{has_extra_info}) {
        my $content_details = $yv_obj->video_details(join(',', map { $yv_utils->get_video_id($_) } @{$videos}), VIDEO_PART);
        my $video_details = $content_details->{results}{items};
        foreach my $i (0 .. $#{$videos}) {
            @{$videos->[$i]}{qw(contentDetails statistics)} = @{$video_details->[$i]}{qw(contentDetails statistics)};
        }
        $results->{has_extra_info} = 1;
    }

    my @formatted;

    foreach my $i (0 .. $#{$videos}) {
        my $video = $videos->[$i];

        if ($opt{results_with_details}) {
            push @formatted,
              ($i == 0 ? '' : "\n")
              . sprintf(
                        "%s. %s\n" . "    %s: %-16s %s: %-13s %s: %s\n" . "    %s: %-12s %s: %-10s %s: %s\n%s\n",
                        colored(sprintf('%2d', $i + 1), 'bold') => colored($yv_utils->get_title($video), 'bold blue'),
                        colored('Views'     => 'bold') => $yv_utils->set_thousands($yv_utils->get_views($video)),
                        colored('Likes'     => 'bold') => $yv_utils->set_thousands($yv_utils->get_likes($video)),
                        colored('Dislikes'  => 'bold') => $yv_utils->set_thousands($yv_utils->get_dislikes($video)),
                        colored('Published' => 'bold') => $yv_utils->get_publication_date($video),
                        colored('Duration'  => 'bold') => $yv_utils->format_time($yv_utils->get_duration($video)),
                        colored('Author'    => 'bold') => $yv_utils->get_channel_title($video),
                        wrap_text(
                                  i_tab => q{ } x 4,
                                  s_tab => q{ } x 4,
                                  text  => [$yv_utils->get_description($video) || 'No description available...']
                                 ),
                       );
        }
        elsif ($opt{results_with_colors}) {
            my $definition = $yv_utils->get_definition($video);
            push @formatted,
              sprintf(
                      "%s. %s (%s) [%s]\n",
                      colored(sprintf('%2d', $i + 1), 'bold'),
                      colored($yv_utils->get_title($video),                            'bold green'),
                      colored("by " . $yv_utils->get_channel_title($video),            'bold yellow'),
                      colored($yv_utils->format_time($yv_utils->get_duration($video)), 'bold bright_blue'),
                     );
        }
        elsif ($opt{results_fixed_width}) {

            state $x = require List::Util;

            my @durations = map { $yv_utils->get_duration($_) } @{$videos};
            my @authors   = map { $yv_utils->get_channel_title($_) } @{$videos};

            my $author_width = List::Util::min(List::Util::max(map { length($_) } @authors), int($term_width / 5));
            my $time_width = List::Util::first(sub { $_ >= 3600 }, @durations) ? 8 : 6;
            my $title_length = $term_width - ($author_width + $time_width + 3 + 2 + 1);

            foreach my $i (0 .. $#{$videos}) {
                my $video = $videos->[$i];
                push @formatted,
                  sprintf("%s. %s %s %*s\n",
                          colored(sprintf('%2d', $i + 1), 'bold'),
                          adj_width($yv_utils->get_title($video), $title_length),
                          adj_width($yv_utils->get_channel_title($video), $author_width, 1),
                          $time_width,
                          $yv_utils->format_time($durations[$i]));
            }
            last;
        }
        else {
            push @formatted,
              sprintf(
                      "%s. %s (by %s) [%s]\n",
                      colored(sprintf('%2d', $i + 1), 'bold'), $yv_utils->get_title($video),
                      $yv_utils->get_channel_title($video), $yv_utils->format_time($yv_utils->get_duration($video)),
                     );
        }
    }

    if ($opt{highlight_watched}) {
        foreach my $i (0 .. $#{$videos}) {
            my $video = $videos->[$i];
            if (exists($watched_videos{$yv_utils->get_video_id($video)})) {
                $formatted[$i] = colored(colorstrip($formatted[$i]), $opt{highlight_color});
            }
        }
    }

    if (@formatted) {
        print "\n" . join("", @formatted);
    }

    if ($opt{play_all} || $opt{play_backwards}) {
        if (@{$videos}) {
            if (
                play_videos(
                            $opt{play_backwards}
                            ? [reverse @{$videos}]
                            : $videos
                           )
              ) {
                if ($opt{play_backwards}) {
                    if (defined $info->{prevPageToken}) {
                        __SUB__->($yv_obj->previous_page($url, $info->{prevPageToken}), auto => 1);
                    }
                    else {
                        $opt{play_backwards} = 0;
                        warn_first_page();
                        __SUB__->($results);
                    }
                }
                else {
                    if (defined $info->{nextPageToken}) {
                        __SUB__->($yv_obj->next_page($url, $info->{nextPageToken}), auto => 1);
                    }
                    else {
                        $opt{play_all} = 0;
                        warn_last_page();
                        __SUB__->($results);
                    }
                }
            }
            else {
                $opt{play_all}       = 0;
                $opt{play_backwards} = 0;
                __SUB__->($results);
            }
        }
        else {
            $opt{play_all}       = 0;
            $opt{play_backwards} = 0;
        }
    }

    state @keywords;
    if ($args{auto}) { }    # do nothing...
    else {
        @keywords = get_input_for_search();

        if (scalar(@keywords) == 0) {    # only arguments
            __SUB__->($results);
        }
    }

    state @for_search;
    state @for_play;

    my @copy_of_keywords = @keywords;
    my $contains_keywords = grep /$non_digit_or_opt_re/, @keywords;

    while (@keywords) {
        my $key = shift @keywords;
        if ($key =~ /$valid_opt_re/) {
            given ($1) {
                when (
                      general_options(
                                      opt => $_,
                                      res => $videos,
                                     )
                  ) {
                }
                when (['help', 'h']) {
                    print $complete_help;
                    press_enter_to_continue();
                }
                when (['n', 'next']) {
                    if (defined $info->{nextPageToken}) {
                        my $request = $yv_obj->next_page($url, $info->{nextPageToken});
                        __SUB__->($request, @keywords ? (auto => 1) : ());
                    }
                    else {
                        warn_last_page();
                        if ($opt{auto_next_page}) {
                            $opt{auto_next_page} = 0;
                            @copy_of_keywords = ();
                            last;
                        }
                    }
                }
                when (['b', 'back', 'p', 'prev', 'previous']) {
                    if (defined $info->{prevPageToken}) {
                        __SUB__->($yv_obj->previous_page($url, $info->{prevPageToken}), @keywords ? (auto => 1) : ());
                    }
                    else {
                        warn_first_page();
                    }
                }
                when (['R', 'refresh']) {
                    @{$videos} = @{$yv_obj->_get_results($url)->{results}{items}};
                }
                when (['r', 'return']) {
                    return;
                }
                when (/^(?:a(?:uthor)?|u)${digit_or_equal_re}(.*)/) {
                    if (my @nums = get_valid_numbers($#{$videos}, $1)) {
                        foreach my $id (@nums) {
                            my $channel_id = $yv_utils->get_channel_id($videos->[$id]);
                            my $request    = $yv_obj->uploads($channel_id);
                            if ($yv_utils->has_entries($request)) {
                                __SUB__->($request);
                            }
                            else {
                                warn_no_results('video');
                            }
                        }
                    }
                    else {
                        warn_no_thing_selected('video');
                    }
                }
                when (/^(?:ps|s2p)${digit_or_equal_re}(.*)/) {
                    if (my @nums = get_valid_numbers($#{$videos}, $1)) {
                        select_and_save_to_playlist(map { $yv_utils->get_video_id($videos->[$_]) } @nums);
                    }
                    else {
                        warn_no_thing_selected('video');
                    }
                }
                when (/^(?:p(?:laylists?)?|up)${digit_or_equal_re}(.*)/) {
                    if (my @nums = get_valid_numbers($#{$videos}, $1)) {
                        foreach my $id (@nums) {
                            my $request = $yv_obj->playlists($yv_utils->get_channel_id($videos->[$id]));
                            if ($yv_utils->has_entries($request)) {
                                print_playlists($request);
                            }
                            else {
                                warn_no_results('playlist');
                            }
                        }
                    }
                    else {
                        warn_no_thing_selected('video');
                    }
                }
                when (/^((?:dis)?like)${digit_or_equal_re}(.*)/) {
                    my $rating = $1;
                    if (my @nums = get_valid_numbers($#{$videos}, $2)) {
                        rate_videos($rating, map { $yv_utils->get_video_id($videos->[$_]) } @nums);
                    }
                    else {
                        warn_no_thing_selected('video');
                    }
                }
                when (/^(?:fav(?:orite)?|F)${digit_or_equal_re}(.*)/) {
                    if (my @nums = get_valid_numbers($#{$videos}, $1)) {
                        favorite_videos(map { $yv_utils->get_video_id($videos->[$_]) } @nums);
                    }
                    else {
                        warn_no_thing_selected('video');
                    }
                }
                when (/^(?:subscribe|S)${digit_or_equal_re}(.*)/) {
                    if (my @nums = get_valid_numbers($#{$videos}, $1)) {
                        subscribe_to_channels(map { $yv_utils->get_channel_id($videos->[$_]) } @nums);
                    }
                    else {
                        warn_no_thing_selected('video');
                    }
                }
                when (/^(?:en)?q(?:ueue)?+${digit_or_equal_re}(.*)/) {
                    if (my @nums = get_valid_numbers($#{$videos}, $1)) {
                        push @{$opt{_queue_play}}, map { $yv_utils->get_video_id($videos->[$_]) } @nums;
                    }
                    else {
                        warn_no_thing_selected('video');
                    }
                }
                when (['pq', 'qp', 'play-queue']) {
                    if (ref $opt{_queue_play} eq 'ARRAY' and @{$opt{_queue_play}}) {
                        my $ids = 'v=' . join(q{,}, splice @{$opt{_queue_play}});
                        general_options(opt => $ids);
                    }
                    else {
                        warn colored("\n[!] The playlist is empty!", 'bold red') . "\n";
                    }
                }
                when (/^c(?:omments?)?${digit_or_equal_re}(.*)/) {
                    if (my @nums = get_valid_numbers($#{$videos}, $1)) {
                        get_and_print_comments(map { $yv_utils->get_video_id($videos->[$_]) } @nums);
                    }
                    else {
                        warn_no_thing_selected('video');
                    }
                }
                when (/^r(?:elated)?${digit_or_equal_re}(.*)/) {
                    if (my ($id) = get_valid_numbers($#{$videos}, $1)) {
                        get_and_print_related_videos($yv_utils->get_video_id($videos->[$id]));
                    }
                    else {
                        warn_no_thing_selected('video');
                    }
                }
                when (/^d(?:ownload)?${digit_or_equal_re}(.*)/) {
                    if (my @nums = get_valid_numbers($#{$videos}, $1)) {
                        local $opt{download_video} = 1;
                        play_videos([@{$videos}[@nums]]);
                    }
                    else {
                        warn_no_thing_selected('video');
                    }
                }
                when (/^(?:play|P)${digit_or_equal_re}(.*)/) {
                    if (my @nums = get_valid_numbers($#{$videos}, $1)) {
                        local $opt{download_video} = 0;
                        local $opt{extract_info}   = undef;
                        play_videos([@{$videos}[@nums]]);
                    }
                    else {
                        warn_no_thing_selected('video');
                    }
                }
                when (/^i(?:nfo)?${digit_or_equal_re}(.*)/) {
                    if (my @nums = get_valid_numbers($#{$videos}, $1)) {
                        foreach my $num (@nums) {
                            print_video_info($videos->[$num]);
                        }
                        press_enter_to_continue();
                    }
                    else {
                        warn_no_thing_selected('video');
                    }
                }
                when (['anp']) {    # auto-next-page
                    $opt{auto_next_page} = 1;
                }
                when (['nnp']) {    # no-next-page
                    $opt{auto_next_page} = 0;
                }
                when (/^[ks]re(?:gex)?=(.*)/) {
                    my $value = $1;
                    if ($value =~ /^([a-zA-Z]++)(?>,|=>)(.+)/) {
                        play_videos_matched_by_regex(
                                                     key    => $1,
                                                     regex  => $2,
                                                     videos => $videos,
                                                    )
                          or __SUB__->($results);
                    }
                    else {
                        warn_invalid("Special Regexp", $value);
                    }
                }
                when (/^re(?:gex)?=(.*)/) {
                    play_videos_matched_by_regex(
                                                 key    => 'title',
                                                 regex  => $1,
                                                 videos => $videos,
                                                )
                      or __SUB__->($results);
                }
                default {
                    warn_invalid('option', $_);
                }
            }
        }
        else {
            given ($key) {
                when (\&youtube_urls) { }    # do nothing
                when (!$contains_keywords && (valid_num($_, $videos) || /$range_num_re/)) {
                    my @for_play;
                    if (/$range_num_re/) {
                        my $from = $1;
                        my $to = $2 // do {
                            $opt{auto_next_page} ? do { $from = 1 } : do { $opt{auto_next_page} = 1 };
                            $#{$videos} + 1;
                        };
                        my @ids = get_valid_numbers($#{$videos}, "$from..$to");
                        continue if not @ids;
                        push @for_play, @ids;
                    }
                    else {
                        push @for_play, $_ - 1;
                    }
                    if (not play_videos([@{$videos}[@for_play]])) {
                        __SUB__->($results);
                    }
                    if ($opt{autohide_watched}) {
                        splice(@{$videos}, $_, 1) for @for_play;
                    }
                }
                default {
                    push @for_search, $_;
                }
            }
        }
    }

    if (@for_search) {
        __SUB__->($yv_obj->search_videos([splice(@for_search)]));
    }
    elsif ($opt{auto_next_page}) {
        @keywords = (':next', grep { not $_ ~~ [qw(:n :next :anp)] } @copy_of_keywords);

        if (@keywords > 1) {
            my $timeout = 2;
            print colored("\n[*] Press <ENTER> in $timeout seconds to stop the :anp option.", 'bold green');
            eval {
                local $SIG{ALRM} = sub {
                    die "alarm\n";
                };
                alarm $timeout;
                scalar <STDIN>;
                alarm 0;
            };

            if ($@) {
                if ($@ eq "alarm\n") {
                    __SUB__->($results, auto => 1);
                }
                else {
                    warn colored("\n[!] Unexpected error: <$@>.", 'bold red') . "\n";
                }
            }
            else {
                $opt{auto_next_page} = 0;
                __SUB__->($results);
            }
        }
        else {
            warn colored("\n[!] Option ':anp' works only combined with other options!", 'bold red') . "\n";
            $opt{auto_next_page} = 0;
            __SUB__->($results);
        }
    }

    __SUB__->($results) if not $args{auto};

    return 1;
}

sub press_enter_to_continue {
    scalar $term->readline(colored("\n=>> Press ENTER to continue...", 'bold'));
}

sub main_quit {
    exit($_[0] // 0);
}

main_quit(0);
