#!/usr/bin/perl 

eval 'exec /usr/bin/perl  -S $0 ${1+"$@"}'
    if 0; # not running under some shell
#
# Copyright (C) 2010-2013 Trizen <echo dHJpemVueEBnbWFpbC5jb20K | base64 -d>.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# 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 the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
#-------------------------------------------------------
#  Appname: youtube-viewer
#  Created on: 02 June 2010
#  Latest edit on: 06 September 2013
#  Websites: http://trizen.googlecode.com
#            https://github.com/trizen/youtube-viewer
#-------------------------------------------------------
#
# youtube-viewer is a command line utility for viewing youtube-videos in MPlayer.
#
# [CHANGELOG]
# - Added support for YouTube EDU categories (-edu) // Options: :dv, :lec, :courses, :course   - NEW (v3.0.4)
# - Some minor bug-fixes // New options has been added: --pp, :pp, :anp, :kregex               - NEW (v3.0.3)
# - Added support for more resolutions, UTF-8 support, --convert-to=FMT and many bug-fixes     - NEW (v3.0.1)
# - Youtube Viewer 3.0 has been released! New options, better functionality and new bugs :)    - NEW (v3.0.0)
# - Added support for detailed results (usage: -D or --details) // Support for comments        - NEW (v2.5.8)
# - Switched to Term::ReadLine for a better STDIN support // Better colors // Info support     - NEW (v2.5.7)
# - Added support for: -duration, -caption=s, -safe-search=s, -hd // Improved code quality     - NEW (v2.5.6)
# - Added support for configuration file, improved stability, improved debug mode              - NEW (v2.5.5)
# - Switched to XML::Fast for parsing gdata XML, in consequence, youtube-viewer is faster!     - NEW (v2.5.5)
# - Switched to Getopt::Long, added SIGINT handler and a better way to execute mplayer         - NEW (v2.5.5)
# - Added support to list playlists created by a specific user (usage: -up <USERNAME>)         - NEW (v2.5.4)
# - Improved parsing support for arguments, including arguments specified via STDIN.           - NEW (v2.5.4)
# - Added support to search for videos uploaded by a particular YouTube user (-author=USER)    - NEW (v2.5.4)
# - Added support to get video results starting with a predefined page (e.g.: -page=4)         - NEW (v2.5.4)
# - Added support for previous page and support to list youtube usernames from a file          - (v2.5.2)
# - Added few options to control cache of MPlayer and lower cache for lower video resolutions  - (v2.5.1)
# - 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 it's resolution is higher. Ex: [url] --480   - (v2.2.*)
# - Added a nice feature which prints some informations 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 for script to choose automat quality if it is lower than 1080p               - (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.*)

# 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 support for proxy - https://github.com/Ulrar)

=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-2013 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.

See http://dev.perl.org/licenses/ for more information.

=cut

use 5.016;

#use lib qw(../lib);    # devel only
#use warnings;          # debug only

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

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

my $appname  = 'Youtube Viewer';
my $version  = '3.0.8';
my $execname = 'youtube-viewer';

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

# Developer key
my $key = 'eTj9NtCyOsGMliTTwz-T85muGT-ARAwVREslfB_giHP3X339Jkpn5Xf71pQXY96xWtFY1oFHt530ct5uZZJk5YTghbNm2IrwZ4';

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

# Unchangeable data goes here
my %constant = (
                dash_line => q{-} x 80,
                win32     => $^O =~ /^mswin\d/i || 0,
               );

my $xdg_config_home = $ENV{XDG_CONFIG_HOME}
  || catdir(
            (
                  $ENV{HOME}
               || $ENV{LOGDIR}
               || ($constant{win32} ? '\Local Settings\Application Data' : ((getpwuid($<))[7] || `echo -n ~`))
            ),
            '.config'
           );

# Configuration dir/file
my $config_dir = catdir($xdg_config_home, $execname);
my $config_file = catfile($config_dir, "$execname.conf");

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

# Locating gcap
my $gcap;
foreach my $path (path()) {
    if (-e (my $gcap_path = catfile($path, 'gcap'))) {
        $gcap = $gcap_path;
        last;
    }
}

# Regular expressions
use WWW::YoutubeViewer::RegularExpressions;

# Main configuration
my %CONFIG = (

    # MPlayer options
    cache             => 30000,
    cache_min         => 5,
    lower_cache       => 2000,
    lower_cache_min   => 3,
    mplayer           => get_mplayer(),
    mplayer_srt_args  => '-sub %s',
    mplayer_arguments => '-prefer-ipv4 -really-quiet -cache %d -cache-min %d',

    # Youtube options
    prefer_webm         => 1,
    prefer_https        => 0,
    results             => 20,
    resolution          => 'original',
    hd                  => undef,
    safe_search         => undef,
    caption             => undef,
    duration            => undef,
    time                => undef,
    orderby             => undef,
    categories_language => 'en-US',

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

    # Subtitle options
    srt_languages => ['en', 'jp'],
    captions_dir  => tmpdir(),
    get_captions  => 1,
    gcap          => $gcap,

    # Others
    http_proxy           => undef,
    debug                => 0,
    colors               => $constant{win32} ^ 1,
    clobber              => 0,
    skip_if_exists       => 0,
    lwp_downloading      => $constant{win32},
    fullscreen           => 0,
    use_lower_cache      => 0,
    results_with_details => 0,
    results_with_colors  => 0,
    results_fixed_width  => 0,
    interactive          => 1,
    adj_for_term_width   => $constant{win32} ^ 1,
    download_with_wget   => $constant{win32} ^ 1,
    access_token         => undef,
    refresh_token        => undef,
    thousand_separator   => q{,},
    downloads_folder     => curdir(),
    keep_original_video  => 0,
    download_and_play    => 0,
    remove_played_file   => 0,
    ffmpeg_command       => 'ffmpeg -i %s %s',
    convert_to           => undef,
);

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

my %MPLAYER;

# MPlayer variable arguments
sub set_mplayer_arguments {
    my ($cache, $cache_min) = @_;
    $MPLAYER{mplayer_arguments} = sprintf $opt{mplayer_arguments}, $cache, $cache_min;
    $MPLAYER{fullscreen} = $opt{fullscreen} ? q{-fs}      : q{};
    $MPLAYER{novideo}    = $opt{novideo}    ? q{-novideo} : q{};
    return 1;
}

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]    : list videos from a playlistURL
:playlist=ID      : list videos from a playlistID
:lectures=ID      : list lectures from a categoryID
:course=ID        : list lectures from a courseID
:courses=ID       : list 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_help = <<'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 the previous section
:reset, :reload   : restart the application
:dv=i             : print the key=>value informations
-argv -argv2=v    : some arguments (e.g.: -u=google)
: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 just like a single value/keyword.
NOTES

my $general_help = <<"HELP";

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

my $complete_help = <<"STDIN_HELP";

$base_options
$control_help
$action_options
# YouTube
:i(nfo)=i,i       : show video informations
:d(ownload)=i,i   : download the selected videos
:c(omments)=i     : show video comments
:r(elated)=i      : show related videos
:a(uthor)=i       : show a video author's latest uploads
:p(laylists)=i    : show author's latest playlists
:subscribe=i      : subscribe to an 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
:q(ueue)=i,i,...  : enqueue videos to play them later
:pq, :play-queue  : play the enqueued videos (if any)
:anp :nnp         : auto-next-page, no-next-page
: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 - configuration file

EOD

sub dump_configuration {
    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::dump(\%CONFIG);
    print $config_fh $config_documentation, $dumped_config;
    close $config_fh;
}

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

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

if (ref $CONFIG ne 'HASH') {
    $CONFIG = do($config_file) || warn "Can't load the configuration file: $!";
}

my @valid_keys = grep exists $CONFIG{$_}, keys %{$CONFIG};
@CONFIG{@valid_keys} = @{$CONFIG}{@valid_keys};

if (ref $CONFIG ne 'HASH' or not %CONFIG ~~ %{$CONFIG}) {
    dump_configuration();
}

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

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

require WWW::YoutubeViewer;
my $yv_obj = WWW::YoutubeViewer->new(
                                     key           => $key,
                                     access_token  => $CONFIG{access_token},
                                     refresh_token => $CONFIG{refresh_token},
                                     app_name      => $appname,
                                     app_version   => $version,
                                     config_dir    => $config_dir,
                                     escape_utf8   => 1,
                                    );

{
    my $client_id     = '991455593101.apps.googleusercontent.com';
    my $client_secret = 'YcxxWCCbBwIr-IhUDCanrp41';
    my $redirect_uri  = 'urn:ietf:wg:oauth:2.0:oob';

    $yv_obj->set_client_id($client_id);
    $yv_obj->set_client_secret($client_secret);
    $yv_obj->set_redirect_uri($redirect_uri);
}

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

{
    no strict 'refs';
    foreach my $name (qw(description title)) {
        *{__PACKAGE__ . '::get_and_print_video_' . $name} = sub {
            foreach my $id (@_) {
                my $videoID = get_valid_video_id($id) // next;
                my $info = $yv_obj->get_video_info($videoID);
                if (@{$info->{results}}) {
                    say $info->{results}[0]{$name};
                }
                else {
                    warn_cant_get($name, $videoID);
                }
            }
        };
    }
}

# Apply the configuration file
unless (qr/^--?(?>N|noconfig)\z/ ~~ \@ARGV) {
    my (%temp_hash) = %CONFIG;
    apply_configuration(\%temp_hash);
}

#---------------------- YOUTUBE-VIEWER USAGE ----------------------#
sub help {
    my $eqs = q{=} x 30;
    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 Options:
   <url>                : play an YouTube video by URL
   <keywords>           : search and list YouTube videos
   <playlist_url>       : list a playlist of YouTube videos

YouTube options:
   -t  --tops           : show today YouTube video tops
       --tops=all       : show all time YouTube video tops
   -r  --region=ID      : list top videos for a specific region
   -M  --movies         : show YouTube category of movies
   -c  --categories     : show available YouTube categories
       --edu-categories : show the YouTube EDU categories
       --course-id=ID   : list the video lectures from courseID
   -hl --catlang=s      : language for categories (default: en-US)
   -p  --playlists      : search for playlists of videos
       --playlist=ID    : list a playlist of videos by playlistID
       --pp=ID,ID       : play the videos from the given playlist IDs
   -u  --user=s         : list videos uploaded by a specific user
   -up --user-pl=s      : list playlists created by a specific user
   -uf --user-fav=s     : list the videos favorited by a specific user
   -V  --videos=K-WORDS : search for YouTube videos (default mode)
   -id --videoids=ID,ID : play the YouTube videos by video IDs
   -rv --related=URL/ID : show related videos for a video ID/URL
   --channels           : search for Youtube channels
   --disco              : YouTube discovery - search and list the playlist
   --author=s           : search in videos uploaded by a specific user
   --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=s         : search only for videos in a specific category
                          this option can also be used with the --tops argument
   --safe-search=s      : YouTube will skip restricted videos for your location
                          valid values are: none, moderate, strict
   --orderby=s          : order entries by: published, viewCount or rating
   --time=s             : show only videos uploaded within the specified time
                          valid values are: today, this_week and this_month
   --hd                 : search only for videos available in at least 720p
   --page=i             : get results starting with a specific page
   --results=[1-50]     : how many results to display per page
   -2  -3  -4  -7  -1   : resolutions: 240p, 360p, 480p, 720p or 1080p
   --resolution=RES     : resolutions: original, 1080, 720, 480, 360, 340,
                          240, 180, 144
   -F --favorites:s     : show the latest favorited videos *
   -R --recommended     : show the recommended videos for you *
   -S --subscriptions:s : show the new subscription videos *
   -W --watched         : show the latest watched videos on YouTube *
   --channel-sugg       : show the suggested YouTube channels for you *
   --subscribe=USER     : subscribe to a user's channel *
   --favorite=ID/URL    : favorite a YouTube video by URL or ID *
   --like=ID/URL        : send a 'like' rating to one or more videos *
   --dislike=ID/URL     : send a 'dislike' rating to one or more videos *

MPlayer options:
   -f  --fullscreen!    : set the fullscreen mode for mplayer (-fs)
   -n  --novideo!       : play the music only without a video in the foreground
   -l  --use-lc         : use a lower cache for MPlayer (for slow connections)
   --cache=i            : set the cache for MPlayer (set: $opt{cache})
   --cache-min=i        : set the cache-min for MPlayer (set: $opt{cache_min})
   --lcache=i           : set the lower-cache for MPlayer (set: $opt{lower_cache})
   --lcache-min=i       : set the lower-cache-min for MPlayer (set: $opt{lower_cache_min})
   --mplayer=s          : set a media player (set: $opt{mplayer})
   --mplayer-args=s     : replace the arguments for the media player
   --mplayer-srt-args=s : replace the subtitle arguments for the media player
   --append-mplayer=s   : add some arguments for the media player

Other options:
   -d  --download!      : download the video(s)
   -i  --info=ID/URL    : show video informations for a videoID or URL
   -A  --all!           : play all the video results in order
   -s  --shuffle!       : shuffle the playlist before playing (with -A)
   -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
   -L  --lwp-download!  : download the videos with LWP (default: wget)
   -U  --update-config  : update the configuration file before exit
   -N  --noconfig       : start the $appname with the default config
   -I  --interactive!   : prompt for the first user input
   -q  --quiet          : do not display any warning
   -dp --downl-play     : play the video after download (with -d)
   -rp --rem-played     : delete a local video after played (with -dp)
   --really-quiet       : do not display any warning or output
   --use-colors!        : use ANSI colors for text
   --convert-to=FORMAT  : convert video to a specific format (with -d)
   --ffmpeg-command=s   : ffmpeg command for converting videos after download
   --keep-original!     : keep the original video after converting
   --comments=ID/URL    : show comments for a YouTube video
   --get-title=ID/URL   : get and print the title for a video
   --get-desc=ID/URL    : get and print the description for a video
   --gcap=s             : set the full path to the gcap script
   --get-captions!      : download the closed captions (with gcap)
   --captions-dir=s     : the directory where to download the .srt files
   --gdata-url=s        : print video results from a valid GData URL
   --get-term-width     : adjust the text for your terminal width
   --login              : will prompt you for login
   --logout             : will delete the authentication key
   --http_proxy=s       : HTTP proxy to use, format 'http://domain.tld:port/'.
                          If authentication required,
                          use 'http://user:pass\@domain.tld:port/'
   --clobber            : overwrite an existent video (with -d)
   --skip-if-exists!    : don't download videos which already exists (with -d)
   --prefer-https!      : use the HTTPS protocol instead of the HTTP protocol
   --prefer-webm!       : use the WebM format when it is available
   --downloads-dir=s    : downloads directory (set: '$opt{downloads_folder}')

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

NOTE:
    *  -> requires authentication
    !  -> the argument can be negated with '--no'
    =s -> requires an argument
    :s -> can take an optional argument

HELP
    main_quit(0);
}

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

    require Text::Wrap;
    ${Text::Wrap::columns} = $args{columns} || length($constant{dash_line});

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

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

sub tricks {
    my $tab = "\t\t";
    my $cat_ids = wrap_text(
                            i_tab => $tab,
                            s_tab => $tab,
                            text  => [@{WWW::YoutubeViewer::categories_IDs}]
                           );

    my $region_ids = wrap_text(
                               i_tab => $tab,
                               s_tab => $tab,
                               text  => [@{WWW::YoutubeViewer::region_IDs}]
                              );

    my $valid_keys = wrap_text(
        i_tab => q{},
        s_tab => q{ } x 3,
        text  => [
            sort qw(
              favorited videoID
              name duration author
              description dislikes
              rating published views
              category title likes
              )
        ]
    );

    print <<"TRICKS";
** Valid categories:
$cat_ids

** Valid region IDs:
$region_ids

NOTE: Categories and region IDs are case sensitive!
      [orderby, time, duration]'s values are also case sensitive!

** Playing videos
1. To stream the videos in other players, you need to change the
   configuration file. You can execute '$execname -mplayer=vlc' for
   example, but the mplayer arguments doesn't work for vlc.
   The full command should be: $execname -mplayer=vlc -mplayer-arg=-q

2. Available resolutions: original, 1080, 720, 480, 360, 340, 240, 180, 144
   1080 = mp4
   720  = mp4 (unless webm is prefered)
   480  = flv (unless webm is prefered)
   360  = flv (unless webm is prefered)
   340  = mp4 (actually is 360p)
   240  = flv
   180  = 3gpp (mp4v, mp4a)
   144  = 3gpp (mp4v, mp4a)

3. lower_cache and lower_cache_min values are used when -l (--use-lower-cache)
   is specified as argument, or if the resolution of a video is lower than 720p.

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

** More STDIN help:
1. ':r', ':return' will return to the previous section.
   For example, if you search for playlists, then list 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!

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

3. If a stdin option is followed by one or more digits, the equal sign,
   which separates the option from value, can be omited.
   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

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

5. For the option ':kregex', the valid keys are:
   $valid_keys

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

7. ':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!

** Closed-Captions
    To get the closed-captions (subtitles) for videos, you need to install
    the 'gcap' program. See: http://gcap.googlecode.com
    After it is installed, put it into the configuration file as:
        gcap => '/path/to/gcap'

** 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=Music
Results: play all the video results (-A)
         only audio, no video (-n)
         quality 480p (-4)
         search for "russian music"
         in the "Music" category.
         -A will include the videos from the next pages as well.

Command: $execname -comments=http://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=Music --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-by=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)) {
        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(
                             caption results duration
                             author orderby region category
                             categories_language safe_search
                             page debug time prefer_https http_proxy
                             )
      ) {
        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_hd(delete($opt->{hd}) ? 'true' : undef);
    }

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

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

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

    if ($opt->{adj_for_term_width}) {
        adj_for_term_width();
    }

    # ... OTHER OPTIONS ... #
    if (defined $opt->{shuffle_playlist}) {
        require List::Util;
        $opt{shuffle} = delete $opt->{shuffle_playlist};
    }

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

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

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

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

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

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

    if (defined $opt->{dislike_video}) {
        rating_videos('dislike', split(/[,\s]+/, delete $opt->{like_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 (delete $opt->{disco}) {
        if (defined(my $videos = $yv_obj->get_disco_videos($keywords))) {
            print_videos($videos);
        }
        else {
            warn_no_results("disco video");
        }
    }

    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_for_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($value, @{$keywords}));
    }

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

    if (delete $opt->{channel_suggestions}) {
        my $results = $yv_obj->get_channel_suggestions();

        if (defined $results) {
            print_channels($results, 'channel_suggestions');
        }
    }

    if (delete $opt->{show_categories}) {
        print_categories(categories => $yv_obj->get_categories());
    }

    if (delete $opt->{show_educategories}) {
        print_categories(categories => $yv_obj->get_educategories(), edu => 1);
    }

    if (defined $opt->{user_videos}) {
        if ($opt->{user_videos} =~ /$valid_username_re/) {
            print_videos($yv_obj->get_videos_from_username(delete $opt->{user_videos}));
        }
        else {
            warn_invalid("username", $opt->{user_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->get_playlists_from_username(delete $opt->{user_playlists}));
    }

    if (defined $opt->{user_favorited_videos}) {
        my $username = delete $opt->{user_favorited_videos};
        if ($username =~ /$valid_username_re/) {
            print_videos($yv_obj->get_favorited_videos_from_username($username));
        }
        else {
            warn_invalid("username", $username);
            print STDERR "\n";
        }
    }

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

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

    foreach my $feed_name (@{WWW::YoutubeViewer::feed_methods}) {
        if (defined $opt->{$feed_name}) {
            my $user   = delete $opt->{$feed_name};
            my $code   = \&{"WWW::YoutubeViewer::get_$feed_name"};
            my $videos = $yv_obj->$code($user);
            if (defined $videos) {
                print_videos($videos);
            }
        }
    }

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

    if (defined $opt->{get_gdata_url}) {
        my $url = delete $opt->{get_gdata_url};
        if ($url =~ m{^https?://(?:www\.)?gdata\.youtube\.com/feeds/api/}i) {
            print_videos(
                         {
                          url     => $url,
                          results => $yv_obj->get_content($url)
                         }
                        );
        }
        else {
            warn_invalid("GData url", $url);
        }
    }

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

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

    require_getopt_long() unless $opt{getopt_required};
    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},
        'disco|discovery'                            => \$opt{disco},
        'get_title|get-title=s'                      => \$opt{get_title},
        'get-description|get_description=s'          => \$opt{get_description},
        '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{show_tops},
        'c|categories|show-categories'               => \$opt{show_categories},
        'ec|educategories|edu-categories'            => \$opt{show_educategories},
        'channels|search-channels|search_channels:s' => \$opt{search_channels},

        'subscriptions|newsubscriptionvideos|S:s' => \$opt{newsubscriptionvideos},
        'favorites|favorited-videos|F:s'          => \$opt{favorites},
        'recommended|recommendations|R:s'         => \$opt{recommendations},
        'watch_history|watched|w:s'               => \$opt{watch_history},
        'subscribe=s'                             => \$opt{subscribe_channel},
        'favorite=s'                              => \$opt{favorite_video},
        'channel-suggestions'                     => \$opt{channel_suggestions},

        'login|authenticate'     => \$opt{authenticate},
        'logout'                 => \$opt{logout},
        'user|username|u|uv=s'   => \$opt{user_videos},
        'user-playlists|up=s'    => \$opt{user_playlists},
        'user-favorited|uf=s'    => \$opt{user_favorited_videos},
        'related-videos|rl|rv=s' => \$opt{related_videos},
        'http_proxy=s'           => \$opt{http_proxy},

        'catlang|cl|hl=s'              => \$opt{categories_language},
        'category|cat=s'               => \$opt{category},
        'r|region|region_id|country=s' => \$opt{region},

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

        'like=s'                     => \$opt{like_video},
        'dislike=s'                  => \$opt{dislike_video},
        'author=s'                   => \$opt{author},
        'all|A|play-all!'            => \$opt{play_all},
        'use-colors|colors|colored!' => \$opt{colors},
        'prefer-https|prefer_https!' => \$opt{prefer_https},
        'prefer-webm|prefer_webm!'   => \$opt{prefer_webm},

        'noconfig|N'               => \$opt{noconfig},
        'playlists|p|pl:s'         => \$opt{search_playlists},
        'course|course-id=s'       => \$opt{course_id},
        'play-playlists|pp=s'      => \$opt{play_playlists},
        'debug:1'                  => \$opt{debug},
        'download|d!'              => \$opt{download_video},
        'safe-search|safeSearch=s' => \$opt{safe_search},
        'hd|high-definition!'      => \$opt{hd},
        'I|interactive!'           => \$opt{interactive},
        'convert-to|convert_to=s'  => \$opt{convert_to},
        'ffmpeg-command=s'         => \$opt{ffmpeg_command},
        'keep-original-video!'     => \$opt{keep_original_video},

        # MPlayer
        'mplayer=s'                                                 => \$opt{mplayer},
        'cache=i'                                                   => \$opt{cache},
        'cache-min|cache_min=i'                                     => \$opt{cache_min},
        'lcache|lower-cache|lower_cache=i'                          => \$opt{lower_cache},
        'lcache-minlower-cache-min|lower_cache_min=i'               => \$opt{lower_cache_min},
        'use-lower-cache|l|use_lower_cache|use-lc!'                 => \$opt{use_lower_cache},
        'append_mplayer|append-mplayer=s'                           => \$MPLAYER{other_args},
        'mplayer_arguments|mplayer-args|mplayer-arguments=s'        => \$opt{mplayer_arguments},
        'mplayer_srt_args|mplayer-srt-arguments|mplayer-srt-args=s' => \$opt{mplayer_srt_args},

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

        'lwp-download|L!'                   => \$opt{lwp_downloading},
        '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|show-info=s'                => \$opt{show_video_info},
        'get-term-width'                    => \$opt{adj_for_term_width},
        'page=i'                            => \$opt{page},
        'novideo|n!'                        => \$opt{novideo},
        'results=i'                         => \$opt{results},
        'shuffle|s!'                        => \$opt{shuffle_playlist},
        'more|m!'                           => \$opt{more_results},
        'gdata-url=s'                       => \$opt{get_gdata_url},
        'gcap=s'                            => \$opt{gcap},

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

        'thousand-separator=s'           => \$opt{thousand_separator},
        'get-captions|get_captions!'     => \$opt{get_captions},
        'captions-dir|captions_dir=s'    => \$opt{captions_dir},
        'skip-if-exists|skip_if_exists!' => \$opt{skip_if_exists},

        'downloads-dir|downloads_folder|downloads-folder|download-dir=s' => \$opt{downloads_folder},
    );

    apply_configuration(\%opt, $keywords);
}

# Parse the arguments
if (@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(@keywords));
}
elsif (-t and $opt{interactive}) {
    first_user_input();
}
elsif (-t STDOUT and not -t STDIN) {
    print_videos($yv_obj->search(<>));
}
else {
    main_quit($opt{_error});
}

sub require_getopt_long {
    require Getopt::Long;
    Getopt::Long::Configure('no_ignore_case');
    $opt{getopt_required} = 1;
}

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
    }
    else {
        my $mplayer_path = '/usr/bin/mplayer';
        return -x $mplayer_path ? $mplayer_path : q{mplayer}    # *NIX MPlayer
    }
}

# Get term width
sub adj_for_term_width {
    eval { require Term::ReadKey };
    $constant{dash_line} = q{-} x (
                                   $@
                                   ? ((split(q{ }, `stty size`))[1] || 80)
                                   : (Term::ReadKey::GetTerminalSize())[0]
                                  );
    return 1;
}

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 1;
                    }
                    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(@for_search));
        }
        else {
            __SUB__->();
        }
    }
    else {
        __SUB__->();
    }
}

sub get_quotewords {
    require Text::ParseWords;
    return Text::ParseWords::quotewords(@_);
}

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) = $term->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) = @_;

    my $input = $term->readline($text);
    utf8::decode($input);

    $input =~ s/^\s+//;
    $input =~ s/\s+$//;

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

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

    return @{$keywords};
}

sub logout {

    undef $CONFIG{access_token};
    undef $CONFIG{refresh_token};

    dump_configuration();

    $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 $json_data = $yv_obj->oauth_login($code) // return;

    say $json_data if $yv_obj->get_debug;
    my $info = $yv_utils->basic_json_parser($json_data);

    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 = $term->ask_yn(prompt  => colored("\nRemember me", 'bold'),
                                        default => 'y',);
        if ($remember_me) {
            $CONFIG{access_token}  = $yv_obj->get_access_token;
            $CONFIG{refresh_token} = $yv_obj->get_refresh_token;
            dump_configuration();
        }

        return 1;
    }

    return;
}

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

sub favorite_videos {
    return if not authenticated();

    foreach my $id (@_) {
        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 rating_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 {
    foreach my $id (@_) {

        my $videoID = get_valid_video_id($id) // next;
        my $info = $yv_obj->get_video_info($videoID);

        if (@{$info->{results}}) {
            if (not play_videos($info->{results})) {
                return;
            }
        }
        else {
            warn_cant_do('play', $videoID);
        }
    }
    return 1;
}

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

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

        my $videoID = get_valid_video_id($id) // next;
        my $info = $yv_obj->get_video_info($videoID);

        if (@{$info->{results}}) {
            print_video_info($info->{results}[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 $info = $yv_obj->get_related_videos($videoID);

        if (@{$info->{results}}) {
            print_videos($info);
        }
        else {
            warn_cant_get('related videos', $videoID);
        }
    }
    return 1;
}

sub get_and_print_comments {
    foreach my $id (@_) {
        my $videoID = get_valid_video_id($id) // next;
        my $comments = $yv_obj->get_video_comments($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 (@{$info->{results}}) {
            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_videos_from_playlist {
    my ($playlistID) = @_;

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

sub subscribe_to_channels {
    return if not authenticated();

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

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

sub decode_entities {
    require HTML::Entities;
    return HTML::Entities::decode_entities($_[0]);
}

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

sub youtube_urls {
    given (shift) {
        when (/$get_video_id_re/) {
            my $info = $yv_obj->get_video_info($+{video_id});
            play_videos($info->{results});
        }
        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 $mode          = $args{mode};
    my $callback_args = ref $args{args} eq 'ARRAY' ? $args{args} : [];

    adj_for_term_width() if $opt{adj_for_term_width};

    given ($option) {
        when (\&quit_required) {
            main_quit(0);
        }
        when (undef) {
            return;
        }
        when ($_ ~~ ['n', 'next'] and defined $url) {
            if (not $url ~~ $opt{_last_page_urls}) {
                my $request = $yv_obj->next_page($url, (defined($mode) ? ($mode => 1) : ()));
                if (@{$request->{results}}) {
                    $callback->($request, @{$callback_args});
                }
                else {
                    push @{$opt{_last_page_urls}}, $url;
                    warn_last_page();
                }
            }
            else {
                warn_last_page();
            }
        }
        when ($_ ~~ ['b', 'back', 'p', 'prev', 'previous'] and defined $url) {
            if ($yv_obj->back_page_is_available($url)) {
                my $request = $yv_obj->previous_page($url, (defined($mode) ? ($mode => 1) : ()));
                $callback->($request, @{$callback_args});
            }
            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) {
                    foreach my $key (sort keys %{$results->[$num]}) {
                        next if $key ~~ ['description', 'summary'];
                        printf "=>> %-12s: %s\n", $key, $results->[$num]{$key} // q{};
                    }
                }
                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_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 valid_num {
    my ($num, $array_ref) = @_;
    return $num =~ /^[0-9]{1,2}\z/ && $num != 0 && $num <= @{$array_ref};
}

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

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

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

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

        $channel->{title} =~ s{<.*?>}{}sg;
        $channel->{title} = decode_entities($channel->{title})
          if $channel->{title} =~ /&#?\w/;

        if ($opt{results_with_details}) {
            printf(
                   "\n%s. %s\n    %s: %-13s %s: %-7s %s: %-12s\n%s\n",
                   colored(sprintf('%2d', ++$i), 'bold') => colored($channel->{title}, 'bold blue'),
                   colored('Updated'     => 'bold') => $yv_utils->format_date($channel->{updated}),
                   colored('Subscribers' => 'bold') => $channel->{subscribers},
                   colored('Author'      => 'bold') => $channel->{name},
                   wrap_text(
                             i_tab => q{ } x 4,
                             s_tab => q{ } x 4,
                             text  => [$channel->{summary} || 'No description available...']
                            ),
                  );
        }
        else {
            print "\n" if $i == 0;
            printf "%s. %s (by %s)\n", colored(sprintf('%2d', ++$i), 'bold'), $channel->{title}, $channel->{name},;
        }
    }

    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,
                                      mode => ($type // 'channels'),
                                      args => [$type],
                                     )
                  ) {
                }
                when (['h', 'help']) {
                    print $general_help;
                    press_enter_to_continue();
                }
                when (['r', 'return']) {
                    return 1;
                }
                default {
                    warn_invalid('option', $_);
                }
            }
        }
        else {
            given ($key) {
                when (\&youtube_urls) { }    # do nothing
                when (valid_num($_, $channels)) {
                    print_videos($yv_obj->get_videos_from_username($channels->[$_ - 1]{author}));
                }
                default {
                    push @for_search, $_;
                }
            }
        }
    }

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

    __SUB__->(@_);
}

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

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

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

    state $comments_help = <<"HELP";

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

# Control
:n(ext)           : show the next page of comments
:b(ack)           : show the previous page of comments

$action_options
$other_options
$notes_options
HELP

    my $i = 0;
    foreach my $comment (@{$comments}) {
        printf(
               "\n%s on %s said:\n%s\n",
               colored($comment->{name}, 'bold'),
               $yv_utils->format_date($comment->{published}),
               wrap_text(
                         i_tab => q{ } x 4,
                         s_tab => q{ } x 4,
                         text  => [$comment->{content} // 'Empty comment...']
                        ),
              );
    }

    my @keywords = get_input_for_comments();

    my @for_search;
    foreach my $key (@keywords) {
        if ($key =~ /$valid_opt_re/) {
            given ($1) {
                when (
                      general_options(
                                      opt  => $_,
                                      sub  => __SUB__,
                                      url  => $url,
                                      res  => $comments,
                                      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();
                        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->send_comment_to_video($videoID, $comment)) {
                                print "\n** Comment posted!\n";
                            }
                            else {
                                warn colored("\n[!] Your comment has NOT been posted!", 'bold red') . "\n";
                            }
                        }
                    }
                }
                when (['r', 'return']) {
                    return 1;
                }
                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) = @_;

    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();

    my @for_search;
    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 1;
                }
                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 %opts = @_;

    my $track      = $opts{pos} || 0;
    my $categories = $opts{categories};
    my $edu_mode   = $opts{edu};

    return if ref $categories ne 'ARRAY';

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

    foreach my $category (@{$categories}[$track .. $track + $yv_obj->get_results() - 1]) {
        last if not defined $category;
        next unless ref $category eq 'HASH';
        printf "%s. %-40s (id: %s)\n", colored(sprintf('%2d', ++$i), 'bold'), $category->{label}, $category->{term};
    }

    my @keywords = get_input_for_categories();

    foreach my $key (@keywords) {
        if ($key =~ /$valid_opt_re/) {
            given ($1) {
                when (['n', 'next']) {
                    $track += $yv_obj->get_results();
                    unless (exists $categories->[$track]) {
                        warn_last_page();
                        $track -= $yv_obj->get_results();
                    }
                    __SUB__->(pos => $track, categories => $categories, edu => $edu_mode);
                }
                when (['b', 'back', 'p', 'prev', 'previous']) {
                    if ($track >= $yv_obj->get_results()) {
                        $track -= $yv_obj->get_results();
                        __SUB__->(pos => $track, categories => $categories, edu => $edu_mode);
                    }
                    else {
                        warn_first_page();
                    }
                }
                when (general_options(opt => $_, res => $categories,)) { }
                when (['h', 'help']) {
                    print $general_help;
                    press_enter_to_continue();
                }
                when (['r', 'return']) {
                    return 1;
                }
                default {
                    warn_invalid('option', $_);
                }
            }
        }
        else {
            given ($key) {
                when (\&youtube_urls) { }    # do nothing
                when (valid_num($_, $categories)) {
                    my $cat_id = $categories->[$_ + $track - 1]{term};
                    if ($edu_mode) {
                        my $reply = $term->get_reply(
                                                     prompt  => colored('=>> Show courses or lectures?', 'bold green'),
                                                     choices => [qw(courses lectures)],
                                                     default => 'lectures',
                                                    );

                        if ($reply eq 'courses') {
                            print_courses($yv_obj->get_courses_from_category($cat_id));
                        }
                        else {
                            print_videos($yv_obj->get_video_lectures_from_category($cat_id));
                        }
                    }
                    else {
                        print_videos($yv_obj->get_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 1;
                }
                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) = @_;

    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 1;
                }
                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 @{$results->{results}}) {
        warn_no_results('playlist');
    }

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

    state $info_format = <<"FORMAT";

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

    my $num = 0;
    foreach my $playlist (@{$playlists}) {
        if ($opt{results_with_details}) {
            printf(
                   "\n%s. %s\n    %s: %-23s %s: %-7s %s: %s\n%s\n",
                   colored(sprintf('%2d', ++$num), 'bold') => colored($playlist->{title}, 'bold blue'),
                   colored('Updated' => 'bold') => $yv_utils->format_date($playlist->{updated}),
                   colored('Videos'  => 'bold') => $playlist->{count},
                   colored('Author'  => 'bold') => $playlist->{name},
                   wrap_text(
                             i_tab => q{ } x 4,
                             s_tab => q{ } x 4,
                             text  => [$playlist->{summary} || 'No description available...']
                            ),
                  );
        }
        elsif ($opt{results_with_colors}) {
            print "\n" if $num == 0;
            printf(
                   "%s. %s (%s) (%s)\n",
                   colored(sprintf('%2d', ++$num), 'bold'),
                   colored($playlist->{title},     'bold green'),
                   colored("by $playlist->{name}", 'bold yellow'),
                   colored($playlist->{count},     'bold blue'),
                  );
        }
        else {
            print "\n" if $num == 0;
            printf("%s. %s (by %s) (%s)\n",
                   colored(sprintf('%2d', ++$num), 'bold'),
                   $playlist->{title}, $playlist->{name}, $playlist->{count});
        }
    }

    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,
                                      mode => 'playlists',
                                     )
                  ) {
                }
                when (['h', 'help']) {
                    my $help = $control_help;
                    $help .= ":pp=i,i           : play videos from the selected playlists\n";
                    print "\n", $help, $general_help;
                    press_enter_to_continue();
                }
                when (['r', 'return']) {
                    return 1;
                }
                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  => [$playlists->[$id]{summary} || 'No description available...']
                                                );
                            $desc =~ s/^\s+//;
                            printf $info_format, $playlists->[$id]{title}, ($playlists->[$id]{playlistID}) 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 { $_->{playlistID} } @{$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) {
                    get_and_print_videos_from_playlist($playlists->[$_ - 1]{playlistID});
                }
                default {
                    push @for_search, $_;
                }
            }
        }
    }

    if (@for_search) {
        __SUB__->($yv_obj->search_for_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 ($videos, $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 map $_ - 1, grep { $_ > 0 and $_ <= @{$videos} } @output;
}

sub get_streaming_url {
    my ($video_id) = @_;
    my @urls = $yv_obj->get_streaming_urls($video_id);

    my $srt_file;

    my $has_cc;
    foreach my $url (@urls) {
        if (exists $url->{has_cc} and $url->{has_cc} =~ /^(?:true|yes)$/i) {
            $has_cc = 1;
            last;
        }
    }

    # Download the closed-captions
    if ($has_cc and $opt{get_captions} and defined $opt{gcap} and not $opt{novideo}) {
        require WWW::YoutubeViewer::GetCaption;
        my $yv_cap = WWW::YoutubeViewer::GetCaption->new(
                                                         captions_dir => $opt{captions_dir},
                                                         gcap         => $opt{gcap},
                                                         languages    => $CONFIG{srt_languages},
                                                        );
        $srt_file = $yv_cap->get_caption($video_id);
    }

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

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

    if (not defined $streaming) {    # get the highest resolution
        ($streaming, $resolution) = $yv_itags->find_streaming_url(\@urls, $opt{prefer_webm});
    }

    my $info = @urls && ref $urls[-1] eq 'HASH' && exists $urls[-1]{status} ? $urls[-1] : {};

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

sub update_mplayer_arguments {
    my ($resolution) = @_;

    if ($opt{use_lower_cache}
        or not $resolution ~~ [qw(original 1080 720)]) {
        set_mplayer_arguments($opt{lower_cache}, $opt{lower_cache_min});
    }
    else {
        set_mplayer_arguments($opt{cache}, $opt{cache_min});
    }
}

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

    my $title = join(q{ }, split(q{ }, $info->{title}));

    if ($^O ~~ [qw(linux freebsd openbsd)]) {
        $title =~ tr{/}{%};
    }
    else {
        $title =~ tr{:"*/?\\|}{;'+%$%%};
    }

    $title .=
        $streaming->{streaming}{type} =~ /\bmp4\b/i   ? q{.mp4}
      : $streaming->{streaming}{type} =~ /\bflv\b/i   ? q{.flv}
      : $streaming->{streaming}{type} =~ /\bwebm\b/i  ? q{.webm}
      : $streaming->{streaming}{type} =~ /\b3gpp?\b/i ? q{.3gp}
      :                                                 q{.mp4};

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

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

    $title = catfile($opt{downloads_folder}, $title);

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

        if ($opt{download_with_wget} and not $opt{lwp_downloading}) {
            system "wget", ($opt{clobber} ? () : q{-nc}), $streaming->{streaming}{url}, q{-O}, $title;
            return if $?;
        }
        else {
            $yv_obj->lwp_mirror($streaming->{streaming}{url}, $title);
        }
    }

    if (defined $opt{convert_to}) {
        my $output = $title;
        $output =~ s/\.\w{2,5}$//;

        my $new_file = "$output.$opt{convert_to}";
        my $ffmpeg   = sprintf($opt{ffmpeg_command},
                             map { q{"} . s{(["\\`\$])}{\\$1}gr . q{"} }
                             map { rel2abs($_) } $title, $new_file);
        say $ffmpeg if $yv_obj->get_debug;
        system $ffmpeg;

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

        update_mplayer_arguments();
        my @mplayer_line =
          ($opt{mplayer}, get_quotewords(qr/\s+/, 1, join(" ", grep { defined && /\S/ } values %MPLAYER)));

        say "@mplayer_line" if $yv_obj->get_debug;

        system @mplayer_line, $title;
        if ($? == 0 and $opt{remove_played_file}) {
            unlink $title;
        }
    }

    return 1;
}

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

    if ($opt{adj_for_term_width}) {
        adj_for_term_width();
    }

    foreach my $video (@{$videos}) {
        my $streaming = get_streaming_url($video->{videoID});

        if (defined $streaming->{info}{status} and not $streaming->{info}{status} =~ /^(?:ok|success)/i) {
            warn colored("(x_x) Can't stream: " . sprintf($CONFIG{youtube_video_url}, $video->{videoID}), 'bold red')
              . "\n";
            warn colored("(x_x) Status: $streaming->{info}{status}", 'bold red') . "\n\n";
        }

        if (not defined $streaming->{streaming}) {
            next;
        }

        print_video_info($video);

        if ($opt{download_video}) {
            if (not download_video($streaming, $video)) {
                return;
            }
        }
        else {
            update_mplayer_arguments($streaming->{resolution});

            my @mplayer_line = (
                                $opt{mplayer},
                                get_quotewords(
                                               qr/\s+/, 1,
                                               join(
                                                    q{ },
                                                    (
                                                     defined $streaming->{srt_file}
                                                     ? sprintf($opt{mplayer_srt_args}, $streaming->{srt_file})
                                                     : ()
                                                    ),
                                                    grep({defined and /\S/} values %MPLAYER),
                                                   )
                                              )
                               );

            say "@mplayer_line" if $yv_obj->get_debug;

            system @mplayer_line, $streaming->{streaming}{url};
            return if $?;
        }
    }

    return 1;
}

sub play_videos_matched_by_regex {
    my %args = @_;

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

    if (defined(my $re = compile_regex($regex))) {
        if (exists $videos->[0]{$key}) {
            if (my @nums = grep { $videos->[$_]{$key} =~ /$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;
            }
        }
        else {
            warn colored("\n[!] Invalid key: <$key>.", 'bold red'),
              colored("\n[*] Valid keys are: ", 'bold red')
              . colored(join(q{, }, sort keys %{$videos->[0]}), 'bold green') . "\n";
            return;
        }
    }

    return 1;
}

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

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

    print STDOUT sprintf($CONFIG{youtube_video_url}, $info->{videoID});

    my $title_length = length($info->{title});

    print "\n$constant{dash_line}\n",
      q{ } x ((length($constant{dash_line}) - $title_length) / 2 - 4) =>
      (_bold_color("=>> $info->{title} <<=") . "\n\n"),
      map(sprintf(q{** } . "%-*s: %s\n", $opt{_colors} ? 18 : 10, _bold_color($_->[0]), $_->[1]),
          (
           ['Author'    => $info->{author}],
           ['Category'  => $info->{category}],
           ['Duration'  => $yv_utils->format_time($info->{duration})],
           ['Rating'    => sprintf('%.2f', $info->{rating})],
           ['Likes'     => $yv_utils->set_thousands($info->{likes})],
           ['Dislikes'  => $yv_utils->set_thousands($info->{dislikes})],
           ['Favorited' => $yv_utils->set_thousands($info->{favorited})],
           ['Views'     => $yv_utils->set_thousands($info->{views})],
           $info->{published} ? ['Published' => $yv_utils->format_date($info->{published})] : (),
            )),
      "$constant{dash_line}\n";

    return 1;
}

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

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

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

    state $term_width = 80;
    if ($opt{adj_for_term_width} and $opt{results_fixed_width}) {
        adj_for_term_width();
        $term_width = length($constant{dash_line});
    }

    my $num = 0;
    foreach my $video (@{$videos}) {
        if ($opt{results_with_details}) {
            printf(
                   "\n%s. %s\n" . "    %s: %-16s %s: %-12s %s: %s\n" . "    %s: %-12s %s: %-10s %s: %s\n%s\n",
                   colored(sprintf('%2d', ++$num), 'bold') => colored($video->{title}, 'bold blue'),
                   colored('Views'     => 'bold') => $yv_utils->set_thousands($video->{views}),
                   colored('Rating'    => 'bold') => sprintf('%.2f', $video->{rating}),
                   colored('Category'  => 'bold') => $video->{category},
                   colored('Published' => 'bold') => $yv_utils->format_date($video->{published}),
                   colored('Duration'  => 'bold') => $yv_utils->format_time($video->{duration}),
                   colored('Author'    => 'bold') => $video->{author},
                   wrap_text(
                             i_tab => q{ } x 4,
                             s_tab => q{ } x 4,
                             text  => [$video->{description} || 'No description available...']
                            ),
                  );
        }
        elsif ($opt{results_with_colors}) {
            print "\n" if $num == 0;
            printf(
                   "%s. %s (%s) (%s)\n",
                   colored(sprintf('%2d', ++$num), 'bold'),
                   colored($video->{title},                            'bold green'),
                   colored("by $video->{author}",                      'bold yellow'),
                   colored($yv_utils->format_time($video->{duration}), 'bold bright_blue'),
                  );
        }
        elsif ($opt{results_fixed_width}) {

            require List::Util;
            require Text::CharWidth;

            my $max_author_len = List::Util::max(map { length($_->{author}) } @{$videos});
            my $time_width = List::Util::first(sub { $_->{duration} >= 3600 }, @{$videos}) ? 8 : 6;
            my $title_length = $term_width - ($max_author_len + $time_width + 2 + 3 + 1);

            print "\n" if $num == 0;
            foreach my $vid (@{$videos}) {

                my $cp_title = $vid->{title};

                my $title_width_len = Text::CharWidth::mbswidth($cp_title);
                if ($title_width_len != $title_length) {
                    while (Text::CharWidth::mbswidth($cp_title) >= $title_length) {
                        chop $cp_title;
                    }

                    $cp_title .= ' ' x ($title_length - Text::CharWidth::mbswidth($cp_title));
                }

                printf "%s. %s %*s %*s\n", colored(sprintf('%2d', ++$num), 'bold'),
                  $cp_title,
                  $max_author_len, $vid->{author}, $time_width, $yv_utils->format_time($vid->{duration});
            }
            last;
        }
        else {
            print "\n" if $num == 0;
            printf("%s. %s (by %s) (%s)\n",
                   colored(sprintf('%2d', ++$num), 'bold'),
                   $video->{title}, $video->{author}, $yv_utils->format_time($video->{duration}));
        }
    }

    if ($opt{play_all}) {
        if (@{$videos}) {
            if (play_videos($opt{shuffle} ? [List::Util::shuffle(@{$videos})] : $videos)) {
                __SUB__->($yv_obj->next_page($url), auto => 1);
            }
            else {
                $opt{play_all} = 0;
                __SUB__->($results);
            }
        }
        else {
            $opt{play_all} = 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 (not $url ~~ $opt{_last_page_urls}) {
                        my $request = $yv_obj->next_page($url);
                        if (@{$request->{results}}) {
                            __SUB__->($request, @keywords ? (auto => 1) : ());
                        }
                        else {
                            push @{$opt{_last_page_urls}}, $url;
                            warn_last_page();
                            if ($opt{auto_next_page}) {
                                $opt{auto_next_page} = 0;
                                @copy_of_keywords = ();
                                last;
                            }
                        }
                    }
                    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 ($yv_obj->back_page_is_available($url)) {
                        __SUB__->($yv_obj->previous_page($url), @keywords ? (auto => 1) : ());
                    }
                    else {
                        warn_first_page();
                    }
                }
                when (['r', 'return']) {
                    return 1;
                }
                when (/^a(?:uthor)?${digit_or_equal_re}(.*)/) {
                    if (my @nums = get_valid_numbers($videos, $1)) {
                        foreach my $id (@nums) {
                            my $username = $videos->[$id]{author};
                            my $request  = $yv_obj->get_videos_from_username($username);
                            if (@{$request->{results}}) {
                                __SUB__->($request);
                            }
                            else {
                                warn_no_results('video');
                            }
                        }
                    }
                    else {
                        warn_no_thing_selected('video');
                    }
                }
                when (/^p(?:laylists?)?${digit_or_equal_re}(.*)/) {
                    if (my @nums = get_valid_numbers($videos, $1)) {
                        foreach my $id (@nums) {
                            my $username = $videos->[$id]{author};
                            my $request  = $yv_obj->get_playlists_from_username($username);
                            if (@{$request->{results}}) {
                                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)) {
                        rating_videos($rating, map $videos->[$_]{videoID}, @nums);
                    }
                    else {
                        warn_no_thing_selected('video');
                    }
                }
                when (/^fav(?:orite)?+${digit_or_equal_re}(.*)/) {
                    if (my @nums = get_valid_numbers($videos, $1)) {
                        favorite_videos(map $videos->[$_]{videoID}, @nums);
                    }
                    else {
                        warn_no_thing_selected('video');
                    }
                }
                when (/^subscribe${digit_or_equal_re}(.*)/) {
                    if (my @nums = get_valid_numbers($videos, $1)) {
                        subscribe_to_channels(map $videos->[$_]{author}, @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 $_->{videoID}, @{$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 $videos->[$_]{videoID}, @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($videos->[$id]{videoID});
                    }
                    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 colored("\n[!] No video selected for download!", 'bold red') . "\n";
                    }
                }
                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 @ids = get_valid_numbers($videos, "$1..$2");
                        continue if not @ids;
                        push @for_play, @ids;
                    }
                    else {
                        push @for_play, $_ - 1;
                    }
                    if (not play_videos([@{$videos}[@for_play]])) {
                        __SUB__->($results);
                    }
                }
                default {
                    push @for_search, $_;
                }
            }
        }
    }

    if (@for_search) {
        __SUB__->($yv_obj->search(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] }
