Guest User

Untitled

a guest
Jul 24th, 2012
5,658
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. #!/usr/bin/perl
  2. #
  3. # Copyright (C) 2010-2012 Trizen <echo dHJpemVueEBnbWFpbC5jb20K | base64 -d>.
  4. #
  5. # This program is free software: you can redistribute it and/or modify
  6. # it under the terms of the GNU General Public License as published by
  7. # the Free Software Foundation, either version 3 of the License, or
  8. # (at your option) any later version.
  9. #
  10. # This program is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  13. # GNU General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU General Public License
  16. # along with this program.  If not, see <http://www.gnu.org/licenses/>.
  17. #
  18. #-------------------------------------------------------
  19. #  Appname: youtube-viewer
  20. #  Created on: 02 June 2010
  21. #  Latest edit on: 09 May 2012
  22. #  Website: http://trizen.googlecode.com
  23. #-------------------------------------------------------
  24. #
  25. # [?] What is this script for?
  26. #  - This script is useful to search and watch YouTube videos with MPlayer...
  27. #  - Have fun!
  28. #
  29. # [!] The most important changes are written in the changelog!
  30. #
  31. # [CHANGELOG]
  32. # - Added support for detailed results (usage: -D or --details) // Support for comments        - NEW (v2.5.8)
  33. # - Switched to Term::ReadLine for a better STDIN support // Better colors // Info support     - NEW (v2.5.7)
  34. # - Added support for: -duration, -caption=s, -safe-search=s, -hd // Improved code quality     - NEW (v2.5.6)
  35. # - Added support for configuration file, improved stability, improved debug mode              - NEW (v2.5.5)
  36. # - Switched to XML::Fast for parsing gdata XML, in consequence, youtube-viewer is faster!     - NEW (v2.5.5)
  37. # - Switched to Getopt::Long, added SIGINT handler and a better way to execute mplayer         - NEW (v2.5.5)
  38. # - Added support to list playlists created by a specific user (usage: -up <USERNAME>)         - NEW (v2.5.4)
  39. # - Improved parsing support for arguments, including arguments specified via STDIN.           - NEW (v2.5.4)
  40. # - Added support to search for videos uploaded by a particular YouTube user (-author=USER)    - NEW (v2.5.4)
  41. # - Added support to get video results starting with a predefined page (e.g.: -page=4)         - NEW (v2.5.4)
  42. # - Added support for previous page and support to list youtube usernames from a file          - (v2.5.2)
  43. # - Added few options to control cache of MPlayer and lower cache for lower video resolutions  - (v2.5.1)
  44. # - Added colors for text (--use_colors), 360p support (-3), playlist support                  - (v2.5.0)
  45. # - Added support for today and all time Youtube tops (usage: -t, --tops, -a, --all-time)      - (v2.4.*)
  46. # - Re-added the support for the next page / Added support for download (-d, --download)       - (v2.4.*)
  47. # - Added support for Youtube CCaptions. (Depends on: 'gcap' - http://gcap.googlecode.com)     - (v2.4.*)
  48. # - First version with Windows support. Require SMPlayer to play videos. See MPlayer Line      - (v2.4.*)
  49. # - Code has been changed in a proportion of ~60% and optimized for speed // --480 became -4   - (v2.4.*)
  50. # - Added mega-powers of omnibox to the STDIN :)                                               - (v2.3.*)
  51. # - Re-added the option to list and play youtube videos from a user profile. Usage: -u [user]  - (v2.3.*)
  52. # - Added a new option to play only the audio track of a videoclip. Usage: [words] -n          - (v2.3.*)
  53. # - Added option for fullscreen (-f, --fullscreen). Usage: youtube-viewer [words] -f           - (v2.3.*)
  54. # - Added one new option '-c'. It shows available categories and will let you to choose one.   - (v2.3.*)
  55. # - Added one new option '-m'. It shows 3 pages of youtube video results. Usage: [words] -m    - (v2.3.*)
  56. # - For "-A" option has been added 3 pages of youtube video results (50 clips)                 - (v2.3.*)
  57. # - Added "-prefer-ipv4" to the mplayer line (videoclips starts in no time now).               - (v2.3.*)
  58. # - Search and play videos at 480p, 720p. Ex: [words] --480, [words] -A --480                  - (v2.3.*)
  59. # - Added support to play a video at 480p even if it's resolution is higher. Ex: [url] --480   - (v2.2.*)
  60. # - Added a nice feature which prints some informations about the current playing video        - (v2.2.*)
  61. # - Added support to play videos by your order. Example: after search results, insert: 3 5 2 1 - (v2.1.*)
  62. # - Added support for next pages of video results (press <ENTER> after search results)         - (v2.1.*)
  63. # - Added support to continue playing searched videos, usage: "youtube-viewer [words] -A"      - (v2.1.*)
  64. # - Added support to print counted videos and support to insert a number instead of video code - (v2.1.*)
  65. # - Added support to search YouTube Videos in script (e.g.: youtube-viewer avatar trailer)     - (v2.0.*)
  66. # - Added support for script to choose automat quality if it is lower than 1080p               - (v2.0.*)
  67. # - Added support to choose the quality only between 720p and 1080p (if it is available)       - (v2.0.*)
  68. # - Added support for YouTube video codes (e.g.: youtube-viewer WVTWCPoUt8w)                   - (v1.0.*)
  69. # - Added support for 720p and 1080p YouTube Videos...                                         - (v1.0.*)
  70.  
  71. # Special thanks to:
  72. # - Army (for bugs reports and for his great ideas)
  73. # - dhn (for adding youtube-viewer in freshports.org)
  74. # - stressat (for the great review of youtube-viewer: http://stressat.blogspot.com/2012/01/youtube-viewer.html)
  75. # - symbianflo (for packaging youtube-viewer for Mandriva distribution)
  76. # - gotbletu (for the great video review of youtube-viewer: http://www.youtube.com/watch?v=FnJ67oAxVQ4)
  77. # - Julian Ospald for adding youtube-viewer in gentoo portage tree
  78.  
  79. eval 'exec perl -S $0 ${1+"$@"}'
  80.   if 0;    # not running under some shell
  81.  
  82. use 5.010;
  83. use strict;
  84.  
  85. use autouse 'XML::Fast'   => qw(xml2hash);
  86. use autouse 'URI::Escape' => qw(uri_escape);
  87.  
  88. use File::Spec::Functions qw(catdir curdir path rel2abs tmpdir);
  89.  
  90. #-------FOR DEBUG ONLY-------#
  91. # use diagnostics -v;
  92. # use warnings FATAL => 'all';
  93. #----------------------------#
  94.  
  95. my $appname  = 'Youtube Viewer';
  96. my $version  = '2.5.9';
  97. my $execname = 'youtube-viewer';
  98.  
  99. # Configuration dir/file
  100. my $config_dir = (
  101.                   exists $ENV{XDG_CONFIG_HOME}
  102.                   ? $ENV{XDG_CONFIG_HOME}
  103.                   : (   $ENV{HOME}
  104.                      || $ENV{LOGDIR}
  105.                      || (getpwuid($<))[7]
  106.                      || `echo -n ~`)
  107.                     . '/.config'
  108.                  ) . "/$execname";
  109.  
  110. my $config_file = "$config_dir/$execname.conf";
  111. my $noconfig = qr/^--?(?>N|noconfig)$/ ~~ @ARGV;
  112.  
  113. # A better <STDIN> support:
  114. require Term::ReadLine;
  115. my $term = Term::ReadLine->new("$appname $version");
  116.  
  117. # Unchangeable variables goes here
  118. my %constant = (
  119.                 gdata_version => 2,
  120.                 dash_line     => q{-} x 80,
  121.                 win32         => $^O =~ /win|dos/i || 0,
  122.                );
  123.  
  124. # Set $PATH to @path
  125. my @path = path();
  126.  
  127. # Locating gcap
  128. my $gcap;
  129. foreach my $path (@path, @INC) {
  130.     if (-e (my $gcap_path = catdir($path, 'gcap'))) {
  131.         $gcap = $gcap_path;
  132.         last;
  133.     }
  134. }
  135.  
  136. # Get mplayer line
  137. sub get_mplayer {
  138.     if ($constant{win32}) {
  139.         my $smplayer = $ENV{ProgramFiles} . '\\SMPlayer\\mplayer\\mplayer.exe';
  140.         if (-e $smplayer) {
  141.             return $smplayer;    # Windows MPlayer
  142.         }
  143.         else {
  144.             warn "\n\n!!! Please install SMPlayer in order to stream YouTube videos.\n\n";
  145.             return 'mplayer.exe';
  146.         }
  147.     }
  148.     else {
  149.         my $mplayer_path = '/usr/bin/mplayer';
  150.         return -x $mplayer_path ? $mplayer_path : q{mplayer}    # *NIX MPlayer
  151.     }
  152. }
  153.  
  154. # Main configuration
  155. my %CONFIG = (
  156.  
  157.     # MPlayer options
  158.     cache                => 30000,
  159.     cache_min            => 5,
  160.     lower_cache          => 2000,
  161.     lower_cache_min      => 3,
  162.     mplayer              => get_mplayer(),
  163.     mplayer_srt_settings => '-unicode -utf8',
  164.     mplayer_arguments    => '-prefer-ipv4 -really-quiet -cache %d -cache-min %d',
  165.  
  166.     # Youtube options
  167.     results                  => 20,
  168.     resolution               => 1080,
  169.     only_hd_videos           => undef,
  170.     safe_search              => undef,
  171.     only_videos_with_caption => undef,
  172.     duration                 => undef,
  173.     query_parameters         => undef,
  174.     time_sort                => 'all_time',
  175.     order_by                 => 'relevance',
  176.  
  177.     # URI options
  178.     youtube_video_url   => 'http://www.youtube.com/watch?v=%s',
  179.     feeds_main_url      => 'http://gdata.youtube.com/feeds/api',
  180.     get_video_info      => 'http://www.youtube.com/get_video_info',
  181.     google_client_login => 'https://www.google.com/accounts/ClientLogin',
  182.  
  183.     # Subtitle options
  184.     srt_language => 'en',
  185.     tmp_dir      => tmpdir(),
  186.     gcap         => $gcap,
  187.  
  188.     # Others
  189.     use_colors           => 0,
  190.     lwp_downloading      => 0,
  191.     fullscreen           => 0,
  192.     use_lower_cache      => 0,
  193.     results_with_details => 0,
  194.     perl_bin             => $^X,
  195.     thousand_separator   => q{,},
  196.     downloads_folder     => curdir(),
  197.     editor               => $ENV{EDITOR} || 'nano',
  198.     users_list           => "$config_dir/youtube_users.txt",
  199. );
  200.  
  201. my $stdin_help = <<'STDIN_HELP';
  202.  
  203. all               : play all the results in order
  204. next              : go to the next page (same as <ENTER>)
  205. back              : return to the previous page
  206. login             : will prompt you for login
  207. logout            : will delete the authentication key
  208. [integer]         : play the corresponding video
  209. i, info [i]       : show more informations about one video
  210. c, comments [i]   : show video comments (e.g.: c 19)
  211. r, related [i]    : show related videos (e.g.: r 6)
  212. v, videos [i]     : show author's latest videos
  213. p, playlists [i]  : show author's latest playlists
  214. subscribe [i]     : subscribe to author's channel
  215. like, dislike [i] : like or dislike a video
  216. fav, favorite [i] : favorite a video (e.g.: fav 3)
  217. [keywords]        : search for youtube videos
  218. 3-8, 3..8         : same as 3 4 5 6 7 8
  219. 8 2 12 4 6 5 1    : play the videos in your order
  220. -argv -argv2=v    : set some arguments (e.g.: -u=google)
  221. e, edit-config    : edit and apply the configuration
  222. load-config       : (re)load the configuration file
  223. /my?[regex]*$/    : play videos matched by a regex (/i)
  224. reset, reload     : restart the application
  225. q, quit, exit     : close the application
  226. STDIN_HELP
  227.  
  228. my %MPLAYER;
  229.  
  230. # MPlayer variable arguments
  231. sub set_mplayer_arguments {
  232.    my ($cache, $cache_min) = @_;
  233.    $MPLAYER{mplayer_arguments} = sprintf $CONFIG{mplayer_arguments}, $cache, $cache_min;
  234.    $MPLAYER{fullscreen} = $CONFIG{fullscreen} ? q{-fs} : q{};
  235.    return 1;
  236. }
  237.  
  238. # Save hash config to file
  239. sub write_config_to_file {
  240.    Config::save_hash($config_file, \%CONFIG);
  241. }
  242.  
  243. # Creating config unless it exists
  244. unless (-e $config_file) {
  245.    require File::Path;
  246.    File::Path::make_path($config_dir);
  247.    write_config_to_file();
  248. }
  249.  
  250. # itag => resolution
  251. my %itags = (
  252.             37 => 1080,
  253.             22 => 720,
  254.             35 => 480,
  255.             43 => 210,
  256.             34 => 360,
  257.             5  => 240
  258.            );
  259.  
  260. # For YouTube
  261. my $key = 'AI39si5xZtotT-ABtXEHNYpPnfez4T9hfNTkMlWti5gVCbFOZ-wyw70RTTguH_53klpmj3G98sTJGXJF5YY61Zcu1r5XmR2w3Q';
  262. my %lwp_header = ('X-GData-Key' => "key=$key");
  263.  
  264. #----------------------- COLORS -----------------------#
  265. my %c = (
  266.         bold    => q{},
  267.         bred    => q{},
  268.         bgreen  => q{},
  269.         reset   => q{},
  270.         cblack  => q{},
  271.         byellow => q{},
  272.         bpurle  => q{},
  273.         bblue   => q{},
  274.        );
  275.  
  276. if (not $constant{win32}) {    # if not running under Windows
  277.    $c{cblack}  = "\e[40m";                # background black
  278.    $c{byellow} = "\e[1;33m";              # bold yellow
  279.    $c{bpurle}  = "\e[1;35m";              # bold purple
  280.    $c{bblue}   = "\e[1;34m";              # bold blue
  281.    $c{bold}    = "\e[1m";                 # bold terminal color
  282.    $c{bred}    = "\e[1;31m";              # bold red
  283.    $c{bgreen}  = "$c{cblack}\e[1;32m";    # bold green on black background
  284.    $c{reset}   = "\e[0m";                 # reset color
  285. }
  286.  
  287. if (qr/^--?nocolors$/ ~~ \@ARGV) {         # --nocolors
  288.    %c = map { $_ => q{} } keys %c;
  289. }
  290.  
  291. #---------------------- GLOBAL VARIABLES ----------------------#
  292. my $keywords = q{};                        # used to store keywords for search
  293. my $youtube_gdata_url;                     # used to store the gdata URL with API query parameters
  294.  
  295. # Other global variables
  296. my ($lwp, @picks, @results, %streaming);
  297.  
  298. my %prompt = (
  299.     comments_next_page => "$c{reset}\n$c{bold}=>> $c{bgreen}Press <ENTER> for the next page of comments (? - help)$c{reset}\n> ",
  300.     select_video_to_play => "$c{reset}\n$c{bold}=>> $c{bgreen}Insert a number or search something else (? - help)$c{reset}\n> ",
  301.     init_text            => "$c{reset}\n$c{bold}=>> $c{bgreen}Insert an YouTube URL or search something...$c{reset}\n> ",
  302.     email                => "$c{reset}\n$c{bold}=>> $c{bgreen}Email:$c{reset} ",
  303.     password             => "$c{reset}$c{bold}=>> $c{bgreen}Password:$c{reset} ",
  304.     user_from_list       => "$c{reset}\n$c{bold}=>> $c{bgreen}Pick one username$c{reset}\n> ",
  305.     video_playlists      => "$c{reset}\n$c{bold}=>> $c{bgreen}Pick one playlist or search something else$c{reset}\n> ",
  306.     user_playlists       => "$c{reset}\n$c{bold}=>> $c{bgreen}Pick one playlist or insert another username$c{reset}\n> ",
  307.     categories           => "$c{reset}\n$c{bold}=>> $c{bgreen}Pick one category$c{reset}\n> ",
  308.     needs_login          => "$c{bred}You need to login in order to use this feature!$c{reset}\n",
  309. );
  310.  
  311. # Options
  312. my %opt = (start_with_page => 1);
  313.  
  314. #------------------------ REGEXP AREA ------------------------#
  315. my $contains_arguments   = qr/(?>\s|^)-+\w/;
  316. my $match_regexp         = qr{/([^\\/]*(?:\\.[^\\/]*)*)/};
  317. my $valid_playlist_code  = qr/^(?:PL)?([0-9A-Z]{16})$/;
  318. my $get_youtube_code     = qr{\b(?>v|embed|youtu[.]be)(?>[=/]|%3D)([\w\-]{11})};
  319. my $looks_like_gdata_url = qr{^https?://gdata[.]youtube[.]com/feeds/.};
  320.  
  321. # $1 will be the playlist code
  322. my $get_playlist_code = qr{(?:(?:(?>playlist[?]list|view_play_list[?]p)=)|\w#p/c/)(?:PL)?([A-Z0-9]{16})};
  323.  
  324. # $1 will be the Youtube username
  325. my $get_username = qr{^https?://(?:www[.])?youtube[.]com/(?:user/)?(\w{2,})(?:[?].*)?$};
  326.  
  327. # The bellow regex will validate an HTTP url.
  328. # If it is valid, we will try to get an youtube
  329. # video code from that website using /$get_youtube_code/
  330. my $valid_url = qr{^
  331.        ################# This regex will validate an HTTP URL #################
  332.  
  333.                https?://                # http or https followed by ://
  334.                [[:alnum:]]              # first character must be a-zA-Z0-9
  335.                (?:(?:(?:\w*-+\w+|\w+)*  # words, dash, words OR only words
  336.                \.(?=\w))+?              # point if followed by word char
  337.                |                        # OR (validates http://x.yz)
  338.                \.)                      # a single dot
  339.                \w{2,6}                  # domain (words between 2 and 6 chars)
  340.                (?:[#/?!]                # characters after domain
  341.                [#-)+-;=?\\~\w]*)*       # the rest characters of the string
  342. $}x;
  343.  
  344. #---------------------- LOOKING FOR WGET ----------------------#
  345. sub locate_wget {
  346.    foreach my $dir (@path) {
  347.        if (-x (my $wget_path = catdir($dir, 'wget'))) {
  348.            $opt{wget} = $wget_path;
  349.            return 1;
  350.        }
  351.    }
  352.    return;
  353. }
  354.  
  355. #---------------------- LWP::UserAgent ----------------------#
  356. sub set_lwp_useragent {
  357.    require LWP::UserAgent;
  358.    $lwp = 'LWP::UserAgent'->new(
  359.                                 keep_alive    => 1,
  360.                                 env_proxy     => 1,
  361.                                 timeout       => 60,
  362.                                 show_progress => $opt{debug} ? 1 : 0,
  363.                                 agent         => 'Mozilla/5.0 (X11; U; Linux i686; en-US) Chrome/10.0.648.45',
  364.                                );
  365.    $opt{lwp_is_set} = 1;
  366.    binmode *STDOUT, ":encoding(UTF-8)";
  367. }
  368.  
  369. sub lwp_get {
  370.    set_lwp_useragent() unless $opt{lwp_is_set};
  371.    return $lwp->get($_[0], %lwp_header)->content;
  372. }
  373.  
  374. # True if looks like a YouTube code
  375. # XXX: it may not validate any code
  376. sub is_code {
  377.    length $_[0] == 11
  378.      and $_[0]     =~ /^[\w-]{11}$/     # must contain only word chars and dashes
  379.      and $_[0]     =~ /[0-9A-Z_\-]/     # must contain, at least, one 'unusual' char
  380.      and not $_[0] =~ /^--?[a-z]+$/;    # and is not an argument (e.g.: -fullscreen)
  381. }
  382.  
  383. # Set config file to %CONFIG
  384. sub apply_configuration {
  385.    my $config_ref = do $config_file;
  386.    if (ref $config_ref eq 'HASH') {
  387.        while (my ($key, $value) = each %$config_ref) {
  388.            $CONFIG{$key} = $value;
  389.        }
  390.    }
  391.    else {
  392.        warn "\n[!] Configuration file contains some errors!\n",
  393.          "[*] Trying to regenerate the configuration file...\n",
  394.          (write_config_to_file() ? "[*] Done!\n" : "[!] Unable to regenerate $config_file: $!\n");
  395.    }
  396.  
  397.    # Auto-login
  398.    if (defined $CONFIG{auth}) {
  399.        set_auth_key($CONFIG{auth});
  400.    }
  401. }
  402.  
  403. apply_configuration() unless $noconfig;
  404.  
  405. # Convert args (of length of 11) to Youtube URLs
  406. {
  407.    my @argv;
  408.    foreach my $arg (@ARGV) {
  409.        if (is_code($arg)) {
  410.            push @argv, sprintf($CONFIG{youtube_video_url}, $arg);
  411.        }
  412.        else {
  413.            push @argv, $arg;
  414.        }
  415.    }
  416.    @ARGV = @argv;
  417. }
  418.  
  419. # __DIE__ handle
  420. $SIG{__DIE__} = sub {
  421.    if (join(q{}, @_) =~ m{^Can't locate (.+?)\.pm\b}) {    #'
  422.         my $module = $1;
  423.         $module =~ s{[/\\]+}{::}g;
  424.         die <<"REQ";
  425. Module $module is required!\n
  426. To install it, just type in terminal:
  427.     sudo cpan -i $module
  428. REQ
  429.     }
  430.     return 1;
  431. };
  432.  
  433. sub require_getopt_long {
  434.     require Getopt::Long;
  435.     Getopt::Long::Configure('no_ignore_case');
  436.     $opt{required_getopt} = 1;
  437. }
  438.  
  439. # Parsing arguments
  440. if (@ARGV) {
  441.     parse_arguments(@ARGV);
  442. }
  443.  
  444. # Return value for start_index
  445. sub get_start_index {
  446.     return $CONFIG{results} * $opt{start_with_page} - $CONFIG{results} + 1;
  447. }
  448.  
  449. # Returns a list of arguments
  450. sub get_arguments_from_string {
  451.     return grep { chr ord eq q{-} } split(q{ }, shift);
  452. }
  453.  
  454. # Returns a list of keywords
  455. sub get_keywords_from_string {
  456.     return grep { chr ord ne q{-} } split(q{ }, shift);
  457. }
  458.  
  459. # Returns a list of keywords
  460. sub get_keywords_from_array {
  461.     return grep { chr ord ne q{-} } ref $_[0] eq 'ARRAY' ? @{$_[0]} : @_;
  462. }
  463.  
  464. sub parse_arguments {
  465.     require_getopt_long() unless $opt{required_getopt};
  466.     Getopt::Long::GetOptionsFromArray(
  467.         \@_, \%CONFIG,
  468.  
  469.         # Main options
  470.         'help|usage|h|?' => \&help,
  471.         'tricks|tips|T'  => \&tricks,
  472.         'version|V|v'    => \&version,
  473.  
  474.         # Resolutions
  475.         '240p|2'  => sub { $CONFIG{resolution} = 240 },
  476.         '360p|3'  => sub { $CONFIG{resolution} = 360 },
  477.         '480p|4'  => sub { $CONFIG{resolution} = 480 },
  478.         '720p|7'  => sub { $CONFIG{resolution} = 720 },
  479.         '1080p|1' => sub { $CONFIG{resolution} = 1080 },
  480.  
  481.         # Other options
  482.         'categories|c'            => sub { push @{$opt{subs}}, [\&categories_area] },
  483.         'movies|M'                => sub { push @{$opt{subs}}, [\&youtube_movies] },
  484.         'today|t'                 => sub { push @{$opt{subs}}, [\&youtube_tops] },
  485.         'a|all-time'              => sub { push @{$opt{subs}}, [\&youtube_tops, 'all_time'] },
  486.         'subscriptions|S:s'       => sub { push @{$opt{subs}}, [\&get_new_subsc, $_[-1] ? pop() : 'default'] },
  487.         'favorites|favorited|F:s' => sub { push @{$opt{subs}}, [\&get_favorited_videos, $_[-1] ? pop() : 'default'] },
  488.         'recommended|R:s'         => sub { push @{$opt{subs}}, [\&get_recommended_videos, $_[-1] ? pop() : 'default'] },
  489.         'watched|W:s'             => sub { push @{$opt{subs}}, [\&get_watch_history, $_[-1] ? pop() : 'default'] },
  490.         'login'                   => sub { push @{$opt{subs}}, [\&authenticate] },
  491.         'user|u=s'            => sub { push @{$opt{subs}}, [\&videos_from_username,    pop] },
  492.         'user-playlists|up=s' => sub { push @{$opt{subs}}, [\&playlists_from_username, pop] },
  493.         'users:s' => sub { push @{$opt{subs}}, [\&list_user_names => $_[-1] ? pop() : $CONFIG{users_list}] },
  494.  
  495.         'author=s'         => \$opt{author},
  496.         'all|A!'           => \$opt{playback},
  497.         'nocolors'         => \$opt{no_colors},
  498.         'nostdin'          => \$opt{no_stdin},
  499.         'noconfig|N!'      => \$noconfig,
  500.         'playlists|p!'     => \$opt{playlists},
  501.         'debug!'           => \$opt{debug},
  502.         'update-config|U!' => \$opt{update_config},
  503.         'download|d!'      => \$opt{download_video},
  504.         'print_video_id'   => \$opt{print_video_id},
  505.         'safe-search=s'    => \$CONFIG{safe_search},
  506.         'hd!'              => \$CONFIG{only_hd_videos},
  507.         'cache-min=i'      => \$CONFIG{cache_min},
  508.         'colors|C'         => \$CONFIG{use_colors},
  509.         'details|D!'       => \$CONFIG{results_with_details},
  510.         'caption=s'        => \$CONFIG{only_videos_with_caption},
  511.         'sub|lang=s'       => \$CONFIG{srt_language},
  512.         'fs|f!'            => \$CONFIG{fullscreen},
  513.         'l|lower-cache!'   => \$CONFIG{use_lower_cache},
  514.         'query-params|Q=s' => \$CONFIG{query_parameters},
  515.         'lwp-download|L!'  => \$CONFIG{lwp_downloading},
  516.         'order-by=s'       => \$CONFIG{order_by},
  517.         'time=s'           => \$CONFIG{time_sort},
  518.  
  519.         'page=i'           => sub { $opt{start_with_page} = pop },
  520.         'append_mplayer=s' => sub { $MPLAYER{argv}        = pop },
  521.         'novideo|n!'       => sub { $MPLAYER{novideo}     = $_[-1] ? q{-novideo} : q{} },
  522.         'more|m!'          => sub { $CONFIG{results}      = $_[-1] ? 50 : 20 },
  523.  
  524.         'download-dir|downloads-dir=s' => \$CONFIG{downloads_folder},
  525.  
  526.         'quiet|q' => sub {
  527.             close STDOUT;
  528.             close STDERR;
  529.         },
  530.  
  531.         map { defined $CONFIG{$_} && $CONFIG{$_} =~ /^[01]\z/ ? "$_!" : "$_=s" } keys %CONFIG
  532.                                      );
  533.  
  534.     $keywords = join(q{ }, get_keywords_from_array(\@_)) if @_;
  535.  
  536.     # Start with page N
  537.     $opt{start_index} = get_start_index();
  538.  
  539.     if ($opt{no_stdin}) {    # poor implementation of --nostdin
  540.         $term = q{};
  541.         $SIG{__DIE__} = sub { exit 0 };
  542.         close STDERR;
  543.     }
  544.  
  545.     # Dump config
  546.     if ($opt{debug}) {
  547.         print "=>> CONFIG:\n" => Config::_dump(\%CONFIG),
  548.           "=>> MPLAYER:\n"    => Config::_dump(\%MPLAYER);
  549.     }
  550.  
  551.     # Go to selected subroutines
  552.     if (exists $opt{subs}) {
  553.         while (@{$opt{subs}}) {
  554.             my $goto = shift @{$opt{subs}};
  555.             my $sub  = shift @{$goto};
  556.             $sub->(@{$goto});
  557.         }
  558.     }
  559. }
  560.  
  561. $opt{start_index} ||= get_start_index() || 1;
  562.  
  563. #---------------------- PARSING VIDEO CODES SPECIFIED AS ARGUMENTS ----------------------#
  564. foreach my $code (@ARGV) {
  565.     given ($code) {
  566.         when (/$get_playlist_code/o) {
  567.             list_playlist($1);
  568.         }
  569.         when (/$valid_playlist_code/o) {
  570.             list_playlist($1);
  571.         }
  572.         when (/$get_youtube_code/o) {
  573.             $opt{video_id_from_arguments} = 1;
  574.             get_youtube($1);
  575.         }
  576.         when (/$get_username/o) {
  577.             videos_from_username($1);
  578.         }
  579.         when (/$valid_url/o) {
  580.             code_from_content($_);
  581.         }
  582.     }
  583. }
  584.  
  585. sub quit_required {
  586.     $_[0] ~~ ['q', 'quit', 'exit'];
  587. }
  588.  
  589. #---------------------- GO TO insert_url() IF $non_argv is FALSE ----------------------#
  590. sub insert_url {
  591.     {
  592.         given ($term->readline($prompt{init_text})) {
  593.             when (\&check_user_input) {
  594.                 redo;
  595.             }
  596.             when (q{}) {
  597.                 die "\n$c{bred}(x_x) Unable to continue...$c{reset}\n\n";
  598.             }
  599.             default {
  600.                 search(join(q{ }, get_keywords_from_string($_)) or redo);
  601.             }
  602.         }
  603.     }
  604. }
  605. insert_url() unless length $keywords;
  606.  
  607. #---------------------- GET A VIDEO CODE FROM AN WEBSITE CONTENT ----------------------#
  608. sub code_from_content {
  609.     set_lwp_useragent() unless $opt{lwp_is_set};
  610.  
  611.     if ($lwp->get($_[0])->content =~ /$get_youtube_code/o) {
  612.         get_youtube($1);
  613.     }
  614.     else {
  615.         search($_[0]);
  616.     }
  617. }
  618.  
  619. #---------------------- YOUTUBE-VIEWER USAGE ----------------------#
  620. sub help {
  621.     my $eqs = q{=} x 30;
  622.     print <<"HELP";
  623. \n  $eqs $c{bgreen}\U$appname\E$c{reset} $eqs
  624. \t\t\t\t\t\t by Trizen (trizenx\@gmail.com)
  625. \n$c{bold}usage:$c{reset} $execname [options] ([code] || [url] || [keywords])
  626. \n$c{bgreen}Base Options:$c{reset}
  627.    <url>                : play an YouTube video by URL
  628.    <code>               : play an YouTube video by code
  629.    <keywords>           : search and list YouTube videos
  630.    <playlist_url>       : list a playlist of YouTube videos
  631. \n$c{bgreen}YouTube options:$c{reset}
  632.    -t  --today          : show YouTube tops of today
  633.    -a  --all-time       : show YouTube tops of all time
  634.    -c  --categories     : show available YouTube categories
  635.    -p  --playlists      : search for playlists of videos
  636.    -M  --movies         : show YouTube category of movies
  637.    -results=[1-50]      : how many videos to display per page
  638.    -u=s  -user=s        : list videos uploaded by a specific user
  639.    -up=s                : list playlists created by a specific user
  640.    -author=s            : search videos uploaded by a particular user
  641.    -duration=s          : valid values are: short, medium, long
  642.    -caption=s           : valid values are: true, false
  643.    -safe-search=s       : valid values are: none, moderate, strict
  644.    -order-by=s          : order entries by: published, viewCount and rating
  645.    -time=s              : valid values are: today, this_week and this_month
  646.    -page=i              : show video results starting with the page 'i'
  647.    -hd                  : show only the videos available in at least 720p
  648.    -2  -3  -4  -7  -1   : resolution of videos: 240p, 360p, 480p, 720p or 1080p
  649.    -F  --favorites      : show the latest favorited videos *
  650.    -R  --recommended    : show the recommended videos for you *
  651.    -S  --subscriptions  : show the new subscription videos *
  652.    -W  --watched        : show the latest watched videos on YouTube *
  653. \n$c{bgreen}MPlayer options:$c{reset}
  654.    -f  --fullscreen     : set the fullscreen mode for mplayer (-fs)
  655.    -n  --novideo        : play the music only without a video in the foreground
  656.    -l  --lower-cache    : use a lower cache for MPlayer (for slow connections)
  657.    -sub=s   -lang=s     : subtitle language (default: en) (depends on gcap)
  658.    -cache=i             : set the cache for MPlayer (set: $CONFIG{cache})
  659.    -cache-min=i         : set the cache-min for MPlayer (set: $CONFIG{cache_min})
  660.    -mplayer=s           : set a media player (set: $CONFIG{mplayer})
  661.    -mplayer_arguments=s : replace default arguments for the media player
  662.    -append_mplayer=s    : add some arguments for the media player
  663. \n$c{bgreen}Other options:$c{reset}
  664.    -d  --download       : download the video(s)
  665.    -A  --all            : play all the video results in order
  666.    -C  --colors         : use colors to delimit the video results
  667.    -D  --details        : a new look for the results, with more details
  668.    -L  --lwp-download   : download the videos with LWP (default: wget)
  669.    -T  --tricks         : show more 'hidden' features of $appname
  670.    -U  --update-config  : update the configuration file before exit
  671.    -N  --noconfig       : start the $appname with the default config
  672.    -Q  --query-params   : set query parameters (e.g.: 'duration=long&caption')
  673.    -users=file.txt      : list YouTube usernames from a file
  674.    -login               : will prompt you for login
  675.    -downloads-dir       : downloads directory (set: '$CONFIG{downloads_folder}')
  676. \n$c{bgreen}Main options:$c{reset}
  677.    -q  --quiet          : display no output
  678.    -v  --version        : print version and exit
  679.    -h  --help           : print help and exit
  680.  
  681. $c{bold}NOTE:$c{reset}
  682.     * == requires authentication
  683.     -no-[argv] will negate the value of the argument (e.g.: -no-fullscreen)
  684.     each config key is a valid argument (if it's preceded by a dash (-))
  685.  
  686. $c{bgreen}Tips and tricks:$c{reset}
  687.   1. After the search results, press <ENTER> for the next page
  688.   2. After the search results, insert 'back' for the previous page
  689.   3. View more 'hidden' features by executing '$execname -T'\n
  690. HELP
  691.     main_quit();
  692. }
  693.  
  694. #---------------------- YOUTUBE-VIEWER TIPS AND TRICKS ----------------------#
  695. sub tricks {
  696.     print <<"TRICKS";
  697.                         $c{bold}>>$c{reset} $c{bgreen}Tips and tricks$c{reset} $c{bold}<<$c{reset}
  698. \n$c{bold}* $c{bgreen}STDIN arguments:$c{reset}
  699. $stdin_help
  700. \n$c{bold}* $c{bgreen}Did you know that...?$c{reset}
  701.     -A option will play ALL video results, including videos from the next pages;\n
  702.     /REGEXP/ will match case-insensitive (e.g.: /test/ matches 'TeSt');\n
  703.     When multiple videos are selected to play, pressing CTRL+C
  704.         will just empty the playlist and return to the video results.
  705. \n$c{bold}* $c{bgreen}Configuration file$c{reset}
  706.     Since 2.5.5 version, $appname supports a configuration file.
  707.     Config file is: '$config_file'
  708. \n$c{bold}* $c{bgreen}Usage examples:$c{reset}
  709.  ** Show videos uploaded by 'MIT' that matches 'computer science',
  710.     starting with page number 2, in fullscreen mode and 720p resolution.
  711.     % $execname --author=MIT computer science --page=2 -fs --720p\n
  712.  ** Show playlists created by a specific user
  713.     % $execname -up khanacademy\n
  714.  ** Show latest videos (50) uploaded by a specific user and a colorful output
  715.     % $execname -results=50 -u google -C\n
  716. TRICKS
  717.     main_quit();
  718. }
  719.  
  720. # Print version
  721. sub version {
  722.     print "Youtube Viewer $version\n";
  723.     main_quit();
  724. }
  725.  
  726. # ------------------ Authentication ------------------ #
  727.  
  728. sub set_auth_key {
  729.     my ($auth) = @_;
  730.     $lwp_header{Authorization} = "GoogleLogin auth=$auth";
  731. }
  732.  
  733. sub log_out {
  734.     delete $lwp_header{Authorization};
  735.     set_lwp_useragent();
  736. }
  737.  
  738. sub authenticate {
  739.     my ($email, $password);
  740.  
  741.     $email = $term->readline($prompt{email});
  742.     if (defined $CONFIG{password}) {
  743.         $password = $CONFIG{password};
  744.     }
  745.     else {
  746.         if ($constant{win32}) {
  747.             eval { require Term::ReadKey };
  748.             if ($@) {
  749.                 say "[!] Please install Term::ReadKey if you don't want your password to be visible while typing!";
  750.                 $password = $term->readline($prompt{password});
  751.             }
  752.             else {
  753.                 $password = q{};
  754.                 print $prompt{password};
  755.                 Term::ReadKey::ReadMode('noecho');
  756.                 while (ord(my $key = Term::ReadKey::ReadKey(0)) != 10) {
  757.                     $password .= $key;
  758.                 }
  759.                 Term::ReadKey::ReadMode('restore');
  760.             }
  761.         }
  762.         else {
  763.             print $prompt{password};
  764.             system('stty', '-echo');
  765.             chomp($password = <STDIN>);
  766.             system('stty', 'echo');
  767.         }
  768.     }
  769.  
  770.     print "\n\n$c{bold}**$c{reset} Should I save your authentification key into configuration?\n",
  771.       "-> if 'yes', you will be logged automatically next time\n\n",
  772.       "$c{bold}=>>$c{reset} Your answer [y/N]: ";
  773.     my $remember_me = <STDIN> =~ /^y(?:es)?$/i ? 1 : 0;
  774.  
  775.     set_lwp_useragent() unless $opt{lwp_is_set};
  776.     my $resp = $lwp->post(
  777.                           $CONFIG{google_client_login},
  778.                           [Content => 'application/x-www-form-urlencoded',
  779.                            Email   => $email,
  780.                            Passwd  => $password,
  781.                            service => 'youtube',
  782.                            source  => "$appname $version"
  783.                           ]
  784.                          );
  785.  
  786.     if ($resp->{_content} =~ /^Auth=(.+)/m) {
  787.         my $auth = $1;
  788.         if ($remember_me) {
  789.             $CONFIG{auth}       = $auth;
  790.             $opt{update_config} = 1;
  791.         }
  792.         say "\n$c{bold}* $c{bgreen}Logged!$c{reset}";
  793.         set_auth_key($auth);
  794.     }
  795.     else {
  796.         warn "\nUnable to login: $resp->{_content}\n";
  797.         return 0;
  798.     }
  799.     return 1;
  800. }
  801.  
  802. sub get_new_subsc {
  803.     my ($user) = @_;
  804.     unless ($lwp_header{Authorization}) {
  805.         warn $prompt{needs_login};
  806.         authenticate();
  807.     }
  808.     parse_url("$CONFIG{feeds_main_url}/users/$user/newsubscriptionvideos");
  809. }
  810.  
  811. sub get_recommended_videos {
  812.     my ($user) = @_;
  813.     unless ($lwp_header{Authorization}) {
  814.         warn $prompt{needs_login};
  815.         authenticate();
  816.     }
  817.     parse_url("$CONFIG{feeds_main_url}/users/$user/recommendations");
  818. }
  819.  
  820. sub get_favorited_videos {
  821.     my ($user) = @_;
  822.     unless ($lwp_header{Authorization}) {
  823.         warn $prompt{needs_login};
  824.         authenticate();
  825.     }
  826.     parse_url("$CONFIG{feeds_main_url}/users/$user/favorites");
  827. }
  828.  
  829. sub get_watch_history {
  830.     my ($user) = @_;
  831.     unless ($lwp_header{Authorization}) {
  832.         warn $prompt{needs_login};
  833.         authenticate();
  834.     }
  835.     parse_url("$CONFIG{feeds_main_url}/users/$user/watch_history");
  836. }
  837.  
  838. #---------------------- LIST YOUTUBE USERNAMES FROM A FILE ----------------------#
  839. sub list_user_names {
  840.  
  841.     my ($users_file) = @_;
  842.     return unless -T $users_file;
  843.  
  844.     my $i = 0;
  845.     my %usernames_table;
  846.     print "\n";
  847.  
  848.     open my $fh, '<:crlf', $users_file or die $!;
  849.     while (defined(my $username = <$fh>)) {
  850.         next unless $username =~ /^\w+$/;
  851.         chomp $username;
  852.         printf "%s%2d%s - %s%s%s\n", $c{bold}, ++$i, $c{reset}, $c{bgreen}, $username, $c{reset};
  853.         $usernames_table{$i} = $username;
  854.     }
  855.     close $fh;
  856.     {
  857.         given ($term->readline($prompt{user_from_list})) {
  858.             when (\&check_user_input) {
  859.                 redo;
  860.             }
  861.             when (exists $usernames_table{$_}) {
  862.                 videos_from_username($usernames_table{$_});
  863.             }
  864.             when (/^\w+$/) {
  865.                 videos_from_username($_);
  866.             }
  867.             when (/$match_regexp/o) {
  868.                 my $match = qr/$1/i;
  869.                 my ($found, @found) = 0;
  870.                 print "\n";
  871.                 while (my ($number, $username) = each %usernames_table) {
  872.                     if ($username =~ /$match/o) {
  873.                         printf "%s%2d%s - %s%s%s\n", $c{bold}, ++$found, $c{reset}, $c{bgreen}, $username, $c{reset};
  874.                         push @found, $found;
  875.                         $usernames_table{$found} = $username;
  876.                     }
  877.                 }
  878.                 if (@found > 1) {
  879.                     given ($term->readline($prompt{user_from_list})) {
  880.                         when (\&quit_required) {
  881.                             main_quit();
  882.                         }
  883.                         when (exists $usernames_table{$_}) {
  884.                             videos_from_username($usernames_table{$_});
  885.                         }
  886.                         default {
  887.                             insert_url();
  888.                         }
  889.                     }
  890.                 }
  891.                 elsif (@found) {
  892.                     videos_from_username($usernames_table{$found[0]});
  893.                 }
  894.                 continue;
  895.             }
  896.             default {
  897.                 insert_url();
  898.             }
  899.         }
  900.     }
  901. }
  902.  
  903. #---------------------- GET VIDEOS FROM A SPECIFIC USER ----------------------#
  904. sub videos_from_username {
  905.     parse_url("$CONFIG{feeds_main_url}/users/$_[0]/uploads");
  906. }
  907.  
  908. #---------------------- PRINT PLAYLISTS ----------------------#
  909. sub print_playlists {
  910.     my ($playlist, $num) = @_;
  911.  
  912.     # Number, Title, Author, VideosCount
  913.     $CONFIG{use_colors}
  914.  
  915.       # Colorful
  916.       ? printf(
  917.                "%s%s%2d%s%s - %s%s%s%s (%sby %s%s%s) (%s%s%s%s)%s\n",
  918.                $c{cblack} => $c{bold}    => $num                => $c{reset},
  919.                $c{cblack} => $c{byellow} => $playlist->{title}  => $c{reset},
  920.                $c{cblack} => $c{bpurle}  => $playlist->{author} => $c{reset},
  921.                $c{cblack} => $c{bblue}   => $playlist->{count}  => $c{reset},
  922.                $c{cblack} => $c{reset}
  923.               )
  924.       : printf("%s%2d%s - %s (by %s) (%s)\n",
  925.                $c{bold}, $num, $c{reset}, $playlist->{title}, $playlist->{author}, $playlist->{count});
  926. }
  927.  
  928. #---------------------- GET PLAYLISTS FROM A SPECIFIC USER ----------------------#
  929. my $playlist_index;
  930.  
  931. sub playlists_from_username {
  932.     my ($username) = @_;
  933.     search_playlists("$CONFIG{feeds_main_url}/users/$username/playlists?" . default_gdata_arguments(), 'username');
  934. }
  935.  
  936. sub search_playlists {
  937.     return unless @_;
  938.     my ($arg, $mode) = @_;
  939.     $youtube_gdata_url =
  940.         $arg =~ /$looks_like_gdata_url/o
  941.       ? $arg
  942.       : $CONFIG{feeds_main_url} . "/playlists/snippets?q=$arg&" . default_gdata_arguments();
  943.  
  944.     $opt{playlists} = 1;
  945.     parse_content($youtube_gdata_url);
  946.  
  947.     my $i = 0;
  948.     foreach my $playlist (@results) {
  949.         print_playlists($playlist, ++$i);
  950.     }
  951.     playlists_waiting_input($mode);
  952. }
  953.  
  954. #---------------------- SEARCH FOR YOUTUBE PLAYLISTS ----------------------#
  955. sub playlists_waiting_input {
  956.  
  957.     my ($mode) = @_;
  958.     {
  959.         my $prompt =
  960.           defined $mode && $mode eq 'playlists'
  961.           ? $prompt{video_playlists}
  962.           : $prompt{user_playlists};
  963.  
  964.         given ($term->readline($prompt)) {
  965.             when (\&check_user_input) {
  966.                 redo;
  967.             }
  968.             when ([qr/^\s*$/, 'next']) {
  969.                 next_page($mode);
  970.             }
  971.             when ('back') {
  972.                 if (
  973.                     do {
  974.                         $youtube_gdata_url =~ /[&?]start-index=(\d+)/;
  975.                         $1 > $CONFIG{results};
  976.                     }
  977.                   ) {
  978.                     previous_page($mode);
  979.                 }
  980.                 else {
  981.                     continue;
  982.                 }
  983.             }
  984.             when (/^\d+$/ and $_ > 0 and $_ <= @results) {
  985.                 list_playlist($results[$_ - 1]{playlistID});
  986.             }
  987.             default {
  988.                 if ($mode eq 'playlists') {
  989.                     search($_);
  990.                 }
  991.                 elsif ($mode eq 'username') {
  992.                     playlists_from_username($_);
  993.                 }
  994.                 else {
  995.                     print_results();
  996.                 }
  997.             }
  998.         }
  999.     }
  1000. }
  1001.  
  1002. #---------------------- LIST A YOUTUBE PLAYLIST ----------------------#
  1003. sub list_playlist {
  1004.     $opt{playlists} = 0;
  1005.     parse_url("$CONFIG{feeds_main_url}/playlists/$_[0]");
  1006. }
  1007.  
  1008. #---------------------- LIST YOUTUBE MOVIE CATEGORIES ----------------------#
  1009. sub youtube_movies {
  1010.     print "\n";
  1011.     my $i = 0;
  1012.     my %movie_table;
  1013.     foreach my $movie_cat_name ('most_popular', 'most_recent', 'trending') {
  1014.         my $cat_name = ucfirst $movie_cat_name;
  1015.         $cat_name =~ tr/_/ /;
  1016.         print q{ }, $c{bold}, ++$i, "$c{reset} - $cat_name\n";
  1017.         $movie_table{$i} = $movie_cat_name;
  1018.     }
  1019.     {
  1020.         given ($term->readline($prompt{categories})) {
  1021.             when (\&check_user_input) {
  1022.                 redo;
  1023.             }
  1024.             when (exists $movie_table{$_}) {
  1025.                 parse_url("$CONFIG{feeds_main_url}/charts/movies/$movie_table{$_}");
  1026.             }
  1027.         }
  1028.     }
  1029. }
  1030.  
  1031. #---------------------- LIST YOUTUBE TOP VIDEO CATEGORIES ----------------------#
  1032. sub youtube_tops {
  1033.     my $i             = 0;
  1034.     my $today         = $_[0] && $_[0] eq 'all_time' ? 0 : 1;
  1035.     my $standardfeeds = "$CONFIG{feeds_main_url}/standardfeeds";
  1036.     my %tops_table;
  1037.     print "\n";
  1038.     foreach my $cat_top_name (
  1039.                               'top_rated',   'top_favorites',  'most_viewed',    'most_popular',
  1040.                               'most_recent', 'most_discussed', 'most_responded', 'recently_featured'
  1041.       ) {
  1042.         my $top_name = ucfirst $cat_top_name;
  1043.         $top_name =~ tr/_/ /;
  1044.         print q{ }, $c{bold}, ++$i, "$c{reset} - $top_name\n";
  1045.         $tops_table{$i} = $cat_top_name;
  1046.     }
  1047.     {
  1048.         given ($term->readline($prompt{categories})) {
  1049.             when (\&check_user_input) {
  1050.                 redo;
  1051.             }
  1052.             when (exists $tops_table{$_}) {
  1053.                 my $url = "$standardfeeds/$tops_table{$_}";
  1054.                 if ($today and not $url =~ /recent/) {
  1055.                     $url .= '?time=today';
  1056.                 }
  1057.                 parse_url($url);
  1058.             }
  1059.             default {
  1060.                 insert_url();
  1061.             }
  1062.         }
  1063.     }
  1064. }
  1065.  
  1066. #---------------------- LIST YOUTUBE VIDEO CATEGORIES ----------------------#
  1067. sub categories_area {
  1068.     my $n = 0;
  1069.     my %categories_table;
  1070.     print "\n";
  1071.     foreach
  1072.       my $cat (@{xml2hash(lwp_get('http://gdata.youtube.com/schemas/2007/categories.cat'))->{'app:categories'}{'atom:category'}})
  1073.     {
  1074.         next if exists $cat->{'yt:deprecated'};
  1075.         printf "%s%2d%s - %s\n", $c{bold}, ++$n, $c{reset}, $cat->{'-label'};
  1076.         $categories_table{$n} = $cat->{'-term'};
  1077.     }
  1078.     {
  1079.         given ($term->readline($prompt{categories})) {
  1080.             when (\&check_user_input) {
  1081.                 redo;
  1082.             }
  1083.             when (exists $categories_table{$_}) {
  1084.                 parse_url("$CONFIG{feeds_main_url}/videos?category=$categories_table{$_}");
  1085.             }
  1086.             default {
  1087.                 insert_url();
  1088.             }
  1089.         }
  1090.     }
  1091. }
  1092.  
  1093. sub update_mplayer_arguments {
  1094.     if (   $CONFIG{use_lower_cache}
  1095.         or not exists $streaming{720}
  1096.         or $CONFIG{resolution} < 720) {
  1097.         set_mplayer_arguments($CONFIG{lower_cache}, $CONFIG{lower_cache_min});
  1098.     }
  1099.     else {
  1100.         set_mplayer_arguments($CONFIG{cache}, $CONFIG{cache_min});
  1101.     }
  1102. }
  1103.  
  1104. #---------------------- PLAY OR DOWNLOAD AN YOUTUBE VIDEO ----------------------#
  1105. sub play_or_download {
  1106.     my $streaming = shift;
  1107.  
  1108.     print "** STREAMING: $streaming\n\n" if $opt{debug};
  1109.  
  1110.     if ($opt{download_video}) {    # DOWNLOADING
  1111.         my $title = shift @_;
  1112.  
  1113.         if (defined $CONFIG{downloads_folder}) {
  1114.             chdir $CONFIG{downloads_folder};
  1115.         }
  1116.  
  1117.         if (not defined $opt{located_wget} and not $CONFIG{lwp_downloading}) {
  1118.             $opt{located_wget} = locate_wget() || -1;
  1119.         }
  1120.  
  1121.         # Replacing reserved characters with a space
  1122.         $title =~ tr[ "*/:<>?\\|][ ]s;
  1123.  
  1124.        if (not -e "$title.mp4") {
  1125.            if (defined $opt{wget}) {    # Download video with wget
  1126.                system $opt{wget}, q{-nc}, $streaming, '-O', "$title.mp4";
  1127.            }
  1128.            else {                       # Downloading video with LWP
  1129.                say "** Saving to: '$title.mp4'";
  1130.                $lwp->show_progress(1);
  1131.                $lwp->mirror($streaming, "$title.mp4");
  1132.                $lwp->show_progress(0) unless $opt{debug};
  1133.            }
  1134.        }
  1135.        else {
  1136.            warn "** '$title.mp4' already exists...\n";
  1137.        }
  1138.    }
  1139.    else {                               # STREAMING
  1140.  
  1141.        update_mplayer_arguments();      # Update mplayer's arguments
  1142.  
  1143.        my @mplayer_line = split(' ', join(' ', $CONFIG{mplayer}, values %MPLAYER));
  1144.  
  1145.        if ($opt{debug}) {
  1146.            print "** MPlayer Line: @mplayer_line\n\n";
  1147.        }
  1148.        else {
  1149.            system @mplayer_line, $streaming;
  1150.        }
  1151.  
  1152.        # Change directory back to the main working directory
  1153.        chdir delete $constant{cwd} if exists $constant{cwd};
  1154.    }
  1155.    print "\n" unless $opt{video_id_from_arguments};
  1156.  
  1157.    if ($?) {    # if non-zero exit code
  1158.        $opt{playback} = 0;
  1159.        print_results();
  1160.    }
  1161.  
  1162.    if (@picks) {
  1163.        foreach_pick();    # play the next video (if any)
  1164.    }
  1165.    elsif ($opt{playback}) {
  1166.        next_page();
  1167.    }
  1168.    if (@results and not $opt{video_id_from_arguments}) {
  1169.        print_results();    # back to video results
  1170.    }
  1171.    main_quit() unless $opt{video_id_from_arguments};
  1172.    return 1;
  1173. }
  1174.  
  1175. #---------------------- SEARCH YOUTUBE VIDEOS ----------------------#
  1176. if (length($keywords)) {
  1177.    search() unless $opt{video_id_from_arguments};
  1178. }
  1179.  
  1180. sub get_defined_pairs_from_array {
  1181.  
  1182.    # ('arg', undef, 'option', 'value')
  1183.    # --to--
  1184.    # ('option', 'value')
  1185.  
  1186.    foreach (my $i = 0 ; $i <= $#_ ; ++$i) {
  1187.        if (not defined $_[$i]) {
  1188.            splice(@_, --$i, 2);
  1189.        }
  1190.    }
  1191.    return @_;
  1192. }
  1193.  
  1194. sub array_to_gdata_arguments {
  1195.    my @options = &get_defined_pairs_from_array;
  1196.  
  1197.    # ('arg', 'value', 'option', 'true')
  1198.    # --to--
  1199.    # 'arg=value&option=true'
  1200.  
  1201.    my $i   = -2;
  1202.    my $x   = $#options - 1;
  1203.    my $str = q{};
  1204.  
  1205.    while (1) {
  1206.        if ($i + 3 < $x) {
  1207.            $str .= $options[$i += 2] . '=' . $options[$i + 1] . '&';
  1208.        }
  1209.        else {
  1210.            return
  1211.              $str .=
  1212.                $i + 3 == $x ? $options[-3] . '=' . $options[-2] . '&' . $options[-1]
  1213.              : $i + 2 == $x ? $options[-2] . '=' . $options[-1]
  1214.              :                $options[-1];
  1215.        }
  1216.    }
  1217. }
  1218.  
  1219. sub default_gdata_arguments {
  1220.    array_to_gdata_arguments(
  1221.                             'max-results' => $CONFIG{results},
  1222.                             'start-index' => $opt{start_index},
  1223.                             'v'           => $constant{gdata_version},
  1224.                            );
  1225. }
  1226.  
  1227. sub search {
  1228.    $keywords = shift() // $keywords;    #/
  1229.  
  1230.    # Get words which doesn't begins with a dash (-);
  1231.    $keywords = uri_escape(join(q{ }, get_keywords_from_string($keywords)));
  1232.  
  1233.    if ($opt{playlists}) {
  1234.        search_playlists($keywords, 'playlists');
  1235.        return;
  1236.    }
  1237.  
  1238.    $youtube_gdata_url = "$CONFIG{feeds_main_url}/videos?"
  1239.      . array_to_gdata_arguments(
  1240.                                 'q'           => $keywords,
  1241.                                 'max-results' => $CONFIG{results},
  1242.                                 'time'        => $CONFIG{time_sort},
  1243.                                 'orderby'     => $CONFIG{order_by},
  1244.                                 'start-index' => $opt{start_index},
  1245.                                 'safeSearch'  => $CONFIG{safe_search},
  1246.                                 'hd'          => $CONFIG{only_hd_videos} ? 'true' : undef,
  1247.                                 'caption'     => $CONFIG{only_videos_with_caption},
  1248.                                 'duration'    => $CONFIG{duration},
  1249.                                 'author'      => $opt{author},
  1250.                                 'v'           => $constant{gdata_version}
  1251.                                );
  1252.  
  1253.    if (defined $CONFIG{query_parameters}) {
  1254.        unless ($CONFIG{query_parameters} =~ /^&/) {
  1255.            substr($CONFIG{query_parameters}, 0, 0, '&');
  1256.        }
  1257.        $youtube_gdata_url .= $CONFIG{query_parameters};
  1258.    }
  1259.    parse_content($youtube_gdata_url);
  1260.    print_results();
  1261. }
  1262.  
  1263. #---------------------- PREPARE GDATA FEEDS URL ----------------------#
  1264. sub parse_url {
  1265.    ($youtube_gdata_url) = @_;
  1266.    $youtube_gdata_url .= $youtube_gdata_url =~ /\?/ ? '&' : '?';
  1267.    $youtube_gdata_url .= default_gdata_arguments();
  1268.    parse_content($youtube_gdata_url);
  1269.    print_results();
  1270. }
  1271.  
  1272. #---------------------- GET AND PARSE GDATA CONTENT ----------------------#
  1273. sub parse_content {
  1274.  
  1275.    undef @results;
  1276.    my $number = 0;
  1277.    my $hash;
  1278.  
  1279.    eval { $hash = xml2hash(lwp_get($_[0])) };
  1280.  
  1281.    if ($@) {
  1282.        if ($@ =~ /Module \S+ is required!/) {
  1283.            warn $@;
  1284.            main_quit();
  1285.        }
  1286.        else {
  1287.            warn "Error ocurred while parsing $_[0]\n$@\n";
  1288.            exit 1 if ++$opt{retry} == 16;
  1289.            parse_content($_[0]);
  1290.        }
  1291.    }
  1292.  
  1293.    while (
  1294.           my $gdata =
  1295.             ref $hash->{feed}{entry} eq 'ARRAY' ? $hash->{feed}{entry}[$number]
  1296.           : ref $hash->{feed}{entry} eq 'HASH'  ? $hash->{feed}{entry}
  1297.           : $hash->{entry}
  1298.      ) {
  1299.        last unless defined $gdata;
  1300.  
  1301.        push @results, $opt{playlists}
  1302.  
  1303.          # Playlists
  1304.          ? {
  1305.             'playlistID' => $gdata->{'yt:playlistId'},
  1306.             'title'      => $gdata->{title},
  1307.             'author'     => $gdata->{author}{name},
  1308.             'count'      => $gdata->{'yt:countHint'}
  1309.            }
  1310.  
  1311.          # Videos
  1312.          : {
  1313.             'code'        => $gdata->{'media:group'}{'yt:videoid'},
  1314.             'title'       => $gdata->{'media:group'}{'media:title'}{'#text'},
  1315.             'author'      => $gdata->{author}{name},
  1316.             'rating'      => $gdata->{'gd:rating'}{'-average'} || 0,
  1317.             'likes'       => $gdata->{'yt:rating'}{'-numLikes'} || 0,
  1318.             'dislikes'    => $gdata->{'yt:rating'}{'-numDislikes'} || 0,
  1319.             'favorited'   => $gdata->{'yt:statistics'}{'-favoriteCount'},
  1320.             'duration'    => format_time($gdata->{'media:group'}{'yt:duration'}{'-seconds'} || 0),
  1321.             'views'       => $gdata->{'yt:statistics'}{'-viewCount'},
  1322.             'published'   => $gdata->{published},
  1323.             'description' => $gdata->{'media:group'}{'media:description'}{'#text'},
  1324.             'category'    => ref $gdata->{category} eq 'ARRAY' ? $gdata->{category}[1]{'-label'}
  1325.             : $gdata->{category}{'-label'} || 'Unknown',
  1326.            };
  1327.        ++$number;
  1328.        last unless ref $hash->{feed}{entry} eq 'ARRAY';
  1329.    }
  1330.  
  1331.    if ($opt{debug}) {
  1332.        chdir($CONFIG{tmp_dir});
  1333.        open my $fh, '>', "$execname.debug" or die "Unable to write to $execname.debug: $!\n";
  1334.        local $, = "\n";
  1335.        print {$fh} (
  1336.                     "=>> URL:"     => $_[0],
  1337.                     "=>> HASH:"    => Config::_dump($hash),
  1338.                     "=>> Results:" => Config::_dump(\@results)
  1339.                    );
  1340.        close $fh;
  1341.    }
  1342.    print "\n";
  1343. }
  1344.  
  1345. # Format seconds to HH:MM:SS
  1346. sub format_time {
  1347.    $_[0] >= 3600
  1348.      ? join ':', map { sprintf '%02d', $_ } $_[0] / 3600 % 24, $_[0] / 60 % 60, $_[0] % 60
  1349.      : join ':', map { sprintf '%02d', $_ } $_[0] / 60 % 60, $_[0] % 60;
  1350. }
  1351.  
  1352. sub check_user_input {
  1353.    given (shift) {
  1354.        when (\&quit_required) {
  1355.            main_quit();
  1356.        }
  1357.        when (['e', 'edit-config']) {
  1358.            system $CONFIG{editor}, $config_file;
  1359.            apply_configuration;
  1360.            return 1;
  1361.        }
  1362.        when ('load-config') {
  1363.            apply_configuration();
  1364.            return 1;
  1365.        }
  1366.        when ('login') {
  1367.            authenticate();
  1368.            return 1;
  1369.        }
  1370.        when ('logout') {
  1371.            print "Logging out...\n";
  1372.            log_out();
  1373.            return 1;
  1374.        }
  1375.        when (/$contains_arguments/o) {
  1376.            parse_arguments(get_arguments_from_string($_));
  1377.            continue;
  1378.        }
  1379.        when (['reset', 'reload']) {
  1380.            @ARGV = ();
  1381.            do $0;
  1382.        }
  1383.        when (/$get_youtube_code/o) {
  1384.            get_youtube($1);
  1385.        }
  1386.        when (/$get_playlist_code/o) {
  1387.            list_playlist($1);
  1388.        }
  1389.        when (/$valid_playlist_code/o) {
  1390.            list_playlist($_);
  1391.        }
  1392.        when (/$valid_url/o) {
  1393.            code_from_content($_);
  1394.        }
  1395.        when (\&is_code) {
  1396.            get_youtube($_);
  1397.        }
  1398.    }
  1399.    return;
  1400. }
  1401.  
  1402. #---------------------- PRINT VIDEO RESULTS ----------------------#
  1403. sub print_results {
  1404.  
  1405.    unless (@results) {
  1406.        warn "$c{bred}(x_x) No video results!$c{reset}\n";
  1407.        insert_url();
  1408.    }
  1409.  
  1410.    my $num = 0;
  1411.    foreach my $video (@results) {
  1412.        print "$video->{code} - " if $opt{print_video_id};
  1413.        if ($CONFIG{results_with_details}) {    # Results with details (when using --details or -D)
  1414.            printf(
  1415.                   "$c{bold}%2d$c{reset}. $c{bblue}%s$c{reset}\n"
  1416.                     . "    $c{bold}Views:$c{reset} %-16s $c{bold}Rating:$c{reset} %-12s $c{bold}Category:$c{reset} %s\n"
  1417.                     . "    $c{bold}Published:$c{reset} %-12s $c{bold}Duration:$c{reset} %-10s $c{bold}Author:$c{reset} %s\n\n",
  1418.                   ++$num => $video->{title},
  1419.                   set_thousands($video->{views}) => sprintf('%.2f', $video->{rating}) => $video->{category},
  1420.                   format_date($video->{published}) => $video->{duration} => $video->{author}
  1421.                  );
  1422.        }
  1423.        elsif ($CONFIG{use_colors}) {           # Colorful results (when using --colors or -C)
  1424.            printf(
  1425.                   "%s%s%2d%s%s - %s%s%s%s (%sby %s%s%s) (%s%s%s%s)%s\n",
  1426.                   $c{cblack} => $c{bold}    => ++$num             => $c{reset},
  1427.                   $c{cblack} => $c{byellow} => $video->{title}    => $c{reset},
  1428.                   $c{cblack} => $c{bpurle}  => $video->{author}   => $c{reset},
  1429.                   $c{cblack} => $c{bblue}   => $video->{duration} => $c{reset},
  1430.                   $c{cblack} => $c{reset}
  1431.                  );
  1432.        }
  1433.        else {                                  # Normal results
  1434.            printf("%s%2d%s - %s (by %s) (%s)\n",
  1435.                   $c{bold}, ++$num, $c{reset}, $video->{title}, $video->{author}, $video->{duration});
  1436.        }
  1437.    }
  1438.  
  1439.    if ($opt{playback}) {
  1440.        @picks = 1 .. @results;
  1441.        foreach_pick();
  1442.    }
  1443.  
  1444.    {
  1445.        given ($term->readline($prompt{select_video_to_play})) {
  1446.            when (['help', '?']) {
  1447.                print $stdin_help;
  1448.                redo;
  1449.            }
  1450.            when (\&check_user_input) {
  1451.                redo;
  1452.            }
  1453.            when ([qr/^\s*$/, 'next']) {
  1454.                next_page();
  1455.            }
  1456.            when ('back') {
  1457.                if (
  1458.                    do {
  1459.                        $youtube_gdata_url =~ /[&?]start-index=(\d+)/;
  1460.                        $1 > $CONFIG{results};
  1461.                    }
  1462.                  ) {
  1463.                    previous_page();
  1464.                }
  1465.                else {
  1466.                    continue;
  1467.                }
  1468.            }
  1469.            when (/^((?:dis)?like)\s+(\d+)\s*$/) {
  1470.                my ($rating, $i) = ($1, $2);
  1471.                continue if $i == 0 or $i > @results;
  1472.                send_rating_to_video($results[$i - 1]->{code}, $rating);
  1473.                redo;
  1474.            }
  1475.            when (/^c(?:omments)?\s+(\d+)\s*$/) {
  1476.                my $i = $1;
  1477.                continue if $i == 0 or $i > @results;
  1478.                show_comments($results[$i - 1]->{code});
  1479.            }
  1480.            when (/^fav(?:orite)?\s+(\d+)\s*$/) {
  1481.                my $i = $1;
  1482.                continue if $i == 0 or $i > @results;
  1483.                favorite_video($results[$i - 1]->{code});
  1484.                redo;
  1485.            }
  1486.            when (/^r(?:elated(?:[- _]videos)?)?\s+(\d+)\s*$/) {
  1487.                my $i = $1;
  1488.                continue if $i == 0 or $i > @results;
  1489.                show_related_videos($results[$i - 1]->{code});
  1490.            }
  1491.            when (/^sub(?:scribe)?\s+(\d+)\s*$/) {
  1492.                my $i = $1;
  1493.                continue if $i == 0 or $i > @results;
  1494.                subscribe_channel($results[$i - 1]->{author});
  1495.            }
  1496.            when (/^i(?:nfo)?\s+(\d+)\s*$/) {
  1497.                my $i = $1;
  1498.                continue if $i == 0 or $i > @results;
  1499.                $opt{show_info_only} = 1;
  1500.                get_youtube($results[$i - 1]);
  1501.                $opt{show_info_only} = 0;
  1502.                redo;
  1503.            }
  1504.            when (/^v(?:ideos)?\s+(\d+)\s*$/) {
  1505.                my $i = $1;
  1506.                continue if $i == 0 or $i > @results;
  1507.                videos_from_username($results[$i - 1]->{author});
  1508.            }
  1509.            when (/^p(?:laylists)?\s+(\d+)\s*$/) {
  1510.                my $i = $1;
  1511.                continue if $i == 0 or $i > @results;
  1512.                playlists_from_username($results[$i - 1]->{author});
  1513.            }
  1514.            when ('all') {
  1515.                @picks = 1 .. scalar @results;
  1516.                foreach_pick();
  1517.            }
  1518.            when (chr ord eq q{/} and /$match_regexp/o) {
  1519.                my $match = qr/$1/i;
  1520.                @picks = grep { $results[$_ - 1]->{title} =~ /$match/ } 1 .. @results;
  1521.                if (@picks) {
  1522.                    foreach_pick();
  1523.                }
  1524.                else {
  1525.                    warn "\n$c{bold}(X_X) No video matched by the regexp: $c{bgreen}/$match/$c{reset}\n\n";
  1526.                    sleep 1;
  1527.                    print_results();
  1528.                }
  1529.            }
  1530.            when (/\d/ and not /(?:\s|^)[^\d-]/) {
  1531.  
  1532.                # remove numeric arguments (e.g.: -4, -arg=\d);
  1533.                s/(?:\D|^)[-=]+\d+(?:\w+)?//g;
  1534.  
  1535.                # '2..5' or '2-5' to '2 3 4 5', or '3..1 to '3 2 1'
  1536.                s/(\d+)(?:-|\.\.)(\d+)/join q{ }, $1 < $2 ? $1 .. $2 : reverse($2 .. $1);/eg;
  1537.  
  1538.                @picks = grep { /^\d+$/ and $_ > 0 and $_ <= @results } split /[\s,]+/, $_;
  1539.                @picks
  1540.                  ? foreach_pick()
  1541.                  : continue;
  1542.            }
  1543.            default {
  1544.                search(join(q{ }, get_keywords_from_string($_)) or redo);
  1545.            }
  1546.        }
  1547.    }
  1548. }
  1549.  
  1550. sub foreach_pick {
  1551.    while (@picks) {
  1552.        get_youtube($results[shift(@picks) - 1]);
  1553.    }
  1554. }
  1555.  
  1556. #---------------------- NEXT PAGE ----------------------#
  1557. sub next_page {
  1558.    if ($youtube_gdata_url =~ s/[&?]start-index=\K(\d+)/$1 + $CONFIG{results}/e) {
  1559.        if ($opt{playlists}) {
  1560.            search_playlists($youtube_gdata_url, @_);
  1561.        }
  1562.        else {
  1563.            parse_content($youtube_gdata_url);
  1564.            print_results();
  1565.        }
  1566.    }
  1567. }
  1568.  
  1569. #---------------------- PREVIOUS PAGE ----------------------#
  1570. sub previous_page {
  1571.    if ($youtube_gdata_url =~ s/[&?]start-index=\K(\d+)/$1 - $CONFIG{results}/e) {
  1572.        if ($opt{playlists}) {
  1573.            search_playlists($youtube_gdata_url, @_);
  1574.        }
  1575.        else {
  1576.            parse_content($youtube_gdata_url);
  1577.            print_results();
  1578.        }
  1579.    }
  1580. }
  1581.  
  1582. sub lower_quality {
  1583.    foreach my $itag (sort { $b <=> $a } values %itags) {
  1584.        if (exists($streaming{$itag})) {
  1585.            return $streaming{$itag};
  1586.        }
  1587.    }
  1588. }
  1589.  
  1590. sub select_resolution {
  1591.    given ($CONFIG{resolution}) {
  1592.        when (1080) {
  1593.            return $streaming{1080} // lower_quality();
  1594.        }
  1595.        when (720) {
  1596.            return $streaming{720} // lower_quality();
  1597.        }
  1598.        when (480) {
  1599.            return $streaming{480} // lower_quality();
  1600.        }
  1601.        when (360) {
  1602.            return $streaming{360} // lower_quality();
  1603.        }
  1604.        when (240) {
  1605.            return $streaming{240} // lower_quality();    #/
  1606.        }
  1607.        default {
  1608.            return lower_quality();
  1609.        }
  1610.    }
  1611. }
  1612.  
  1613. sub format_itags {
  1614.    my @itags;
  1615.    foreach my $itag (@_) {
  1616.        if (exists($itags{$itag})) {
  1617.            push @itags, $itags{$itag};
  1618.        }
  1619.    }
  1620.    @itags;
  1621. }
  1622.  
  1623. # Getting YouTube closed captions with gcap
  1624. sub get_closed_caption {
  1625.    my ($code) = @_;
  1626.  
  1627.    unless (exists $constant{cwd}) {
  1628.        $constant{cwd} = rel2abs(curdir());
  1629.    }
  1630.  
  1631.    # Change dir to $TMP and get the SRT file
  1632.    chdir $CONFIG{tmp_dir};
  1633.  
  1634.    my $srt_file;
  1635.    my $i = 0;
  1636.    {
  1637.        $srt_file =
  1638.          -e "${code}_$CONFIG{srt_language}.srt"
  1639.          ? "${code}_$CONFIG{srt_language}.srt"
  1640.          : do {
  1641.            opendir(my $dir_h, $CONFIG{tmp_dir}) or return q{};
  1642.            my $srt = (grep /^\Q$code\E[\w-]*[.](?i:srt)\z/, readdir($dir_h))[0];
  1643.            closedir $dir_h;
  1644.            $srt;
  1645.          };
  1646.  
  1647.        unless (defined $srt_file) {
  1648.            system $CONFIG{perl_bin}, $CONFIG{gcap}, $code;
  1649.  
  1650.            if ($? == 0 and not $i++) {
  1651.                redo;
  1652.            }
  1653.        }
  1654.    }
  1655.  
  1656.    return defined $srt_file
  1657.      ? "$CONFIG{mplayer_srt_settings} -sub $srt_file"
  1658.      : q{};
  1659. }
  1660.  
  1661. sub format_date {
  1662.    return "$3.$2.$1"
  1663.      if $_[0] =~ /^(\d{4})-(\d{2})-(\d{2})/;
  1664. }
  1665.  
  1666. # Thousand separator
  1667. sub set_thousands {
  1668.    return 0 unless $_[0];
  1669.    length($_[0]) > 3 or return $_[0];
  1670.    my $n = shift;
  1671.    my $l = length($n) - 3;
  1672.    my $i = ($l - 1) % 3 + 1;
  1673.    my $x = substr($n, 0, $i) . $CONFIG{thousand_separator};
  1674.    while ($i < $l) {
  1675.        $x .= substr($n, $i, 3) . $CONFIG{thousand_separator};
  1676.        $i += 3;
  1677.    }
  1678.    $x . substr($n, $i);
  1679. }
  1680.  
  1681. sub get_youtube {
  1682.    my $info = shift();
  1683.  
  1684.    if (ref $info ne 'HASH') {
  1685.        parse_content("$CONFIG{feeds_main_url}/videos/$info?v=2");
  1686.        $info = $results[0] if @results;
  1687.        if (@results and ref $info eq 'HASH' and not defined $info->{code}) {
  1688.            die <<"ERROR";
  1689. ** Something is REALLY wrong... Unable to continue!\n
  1690. Tips:
  1691.    1. (Edit/delete) the configuration file
  1692.    2. Run in -debug mode and send '${execname}.debug' to developer
  1693. ERROR
  1694.        }
  1695.        elsif (ref $info ne 'HASH') {
  1696.            warn "$c{bred}(x_x) Unable to stream:$c{reset} ", sprintf($CONFIG{youtube_video_url}, $info), "\n\n";
  1697.            return;
  1698.        }
  1699.    }
  1700.  
  1701.    my $code    = $info->{code};
  1702.    my $content = lwp_get("$CONFIG{get_video_info}?&video_id=$code&el=detailpage&ps=default&eurl=&gl=US&hl=en");
  1703.    my $url     = sprintf($CONFIG{youtube_video_url}, $code);
  1704.  
  1705.    $MPLAYER{arguments} = q{};
  1706.    if (    not $opt{download_video}
  1707.        and -e $CONFIG{gcap}
  1708.        and not exists $MPLAYER{novideo}
  1709.        and $content =~ /&has_cc=True&/) {
  1710.        $MPLAYER{arguments} = get_closed_caption($code);
  1711.    }
  1712.  
  1713.    if ($content =~ /url_encoded_fmt_stream_map=(.+?)&/) {
  1714.        my $streaming = $1;
  1715.        $streaming =~ s/%253A/:/gi;
  1716.        $streaming =~ s{%252F}{/}gi;
  1717.        $streaming =~ s/%2526/&/g;
  1718.        $streaming =~ s/%253D/=/gi;
  1719.        $streaming =~ s/%253F/?/gi;
  1720.        $streaming =~ s/%25252C/,/gi;
  1721.  
  1722.        undef %streaming;
  1723.        my (@streaming_urls) =
  1724.          grep m{^https?:}, split(m{url%3D(.+?)%26}, $streaming);
  1725.        @streaming{format_itags(map m{&itag=(\d+)&}, @streaming_urls)} =
  1726.          grep {
  1727.            exists $itags{
  1728.                do { m{&itag=(\d+)&}; $1 }
  1729.              }
  1730.          } @streaming_urls;
  1731.  
  1732.        if ($opt{debug}) {
  1733.            while (my ($key, $value) = each %streaming) {
  1734.                print "KEY = $key\nVALUE = $value\n\n";
  1735.            }
  1736.        }
  1737.        my $rating = sprintf('%.2f', $info->{rating});
  1738.        if (!defined $info->{description}) {
  1739.            $info->{description} = 'No description available...';
  1740.        }
  1741.  
  1742.        print "\n$c{bold}=>>$c{reset} Description\n",
  1743.          "$constant{dash_line}\n",
  1744.          "$info->{description}\n$constant{dash_line}\n",
  1745.          "$c{bold}=>>$c{reset} View & Download\n",
  1746.          "$constant{dash_line}\n$c{bold}* URL:$c{reset} ";
  1747.  
  1748.        print STDOUT $url;
  1749.  
  1750.        print "\n$c{bold}* GET:$c{reset} $streaming_urls[0]\n$constant{dash_line}\n",
  1751.          q{ } x ((length($constant{dash_line}) - length($info->{title})) / 2 - 4),
  1752.          "$c{bold}=>>$c{reset} $c{bgreen}$info->{title}$c{reset} $c{bold}<<=$c{reset}\n\n",
  1753.          "** $c{bold}Author$c{reset}    : $info->{author}\n",
  1754.          "** $c{bold}Category$c{reset}  : $info->{category}\n",
  1755.          "** $c{bold}Duration$c{reset}  : $info->{duration}\n",
  1756.          "** $c{bold}Rating$c{reset}    : $rating\n",
  1757.          "** $c{bold}Likes$c{reset}     : ", set_thousands($info->{likes}) . "\n",
  1758.          "** $c{bold}Dislikes$c{reset}  : ", set_thousands($info->{dislikes}) . "\n",
  1759.          "** $c{bold}Favorited$c{reset} : " . set_thousands($info->{favorited}) . "\n",
  1760.          "** $c{bold}Views$c{reset}     : " . set_thousands($info->{views}) . "\n", do {
  1761.            $info->{published}
  1762.              ? sprintf("** $c{bold}Published$c{reset} : %s\n", format_date($info->{published}))
  1763.              : q{};
  1764.          }, "$constant{dash_line}\n";
  1765.  
  1766.        return if $opt{show_info_only};
  1767.  
  1768.        # Select one resolution and play video
  1769.        play_or_download(select_resolution(), $info->{title});
  1770.    }
  1771.    else {
  1772.  
  1773.        # This happens when a video has been deleted or forbidden
  1774.        warn "\n$c{bred}(x_x) Something went wrong...$c{reset}\n\n", "$c{bred}(x_x) Unable to stream: $c{reset}$url\n\n";
  1775.        if (@results and not $opt{video_id_from_arguments}) {
  1776.            unless (@picks) {
  1777.                sleep 1;
  1778.                print_results();
  1779.            }
  1780.        }
  1781.        else {
  1782.            unless (@results or $opt{video_id_from_arguments}) {
  1783.                main_quit();
  1784.            }
  1785.        }
  1786.    }
  1787. }
  1788.  
  1789. sub show_related_videos {
  1790.    my ($code) = @_;
  1791.    parse_url("$CONFIG{feeds_main_url}/videos/$code/related");
  1792. }
  1793.  
  1794. sub send_rating_to_video {
  1795.    my ($code, $rating) = @_;
  1796.    my $uri = "$CONFIG{feeds_main_url}/videos/$code/ratings";
  1797.  
  1798.    say _save(
  1799.        'POST', $uri, <<"XML_HEADER"
  1800. <?xml version="1.0" encoding="UTF-8"?>
  1801. <entry xmlns="http://www.w3.org/2005/Atom"
  1802.       xmlns:yt="http://gdata.youtube.com/schemas/2007">
  1803. <yt:rating value="$rating"/>
  1804. </entry>
  1805. XML_HEADER
  1806.             ) ? "\u${rating}d!" : 'Error!';
  1807. }
  1808.  
  1809. sub send_comment_to_video {
  1810.    my ($code, $comment) = @_;
  1811.  
  1812.    return unless length $comment;
  1813.  
  1814.    my $uri = "$CONFIG{feeds_main_url}/videos/$code/comments";
  1815.  
  1816.    say _save(
  1817.        'POST', $uri, <<"XML_HEADER"
  1818. <?xml version="1.0" encoding="UTF-8"?>
  1819. <entry xmlns="http://www.w3.org/2005/Atom"
  1820.    xmlns:yt="http://gdata.youtube.com/schemas/2007">
  1821.  <content>$comment</content>
  1822. </entry>
  1823. XML_HEADER
  1824.             ) ? 'Comment sent' : 'Error!';
  1825. }
  1826.  
  1827. sub subscribe_channel {
  1828.    my ($user) = @_;
  1829.    my $uri = "$CONFIG{feeds_main_url}/users/default/subscriptions";
  1830.  
  1831.    say _save(
  1832.        'POST', $uri, <<"XML_HEADER"
  1833. <?xml version="1.0" encoding="UTF-8"?>
  1834. <entry xmlns="http://www.w3.org/2005/Atom"
  1835.  xmlns:yt="http://gdata.youtube.com/schemas/2007">
  1836.    <category scheme="http://gdata.youtube.com/schemas/2007/subscriptiontypes.cat"
  1837.      term="channel"/>
  1838.    <yt:username>$user</yt:username>
  1839. </entry>
  1840. XML_HEADER
  1841.             ) ? "Successfully subscribed to user: $user" : 'Error!';
  1842. }
  1843.  
  1844. sub favorite_video {
  1845.    my ($code) = @_;
  1846.    my $uri = "$CONFIG{feeds_main_url}/users/default/favorites";
  1847.  
  1848.    say _save(
  1849.        'POST', $uri, <<"XML_HEADER"
  1850. <?xml version="1.0" encoding="UTF-8"?>
  1851. <entry xmlns="http://www.w3.org/2005/Atom">
  1852.  <id>$code</id>
  1853. </entry>
  1854. XML_HEADER
  1855.             ) ? "** Successfully favorited video: $code" : 'Error!';
  1856. }
  1857.  
  1858. sub _request {
  1859.    my ($req) = @_;
  1860.  
  1861.    my $res = $lwp->request($req);
  1862.    if ($res->is_success) {
  1863.        return $res->content();
  1864.    }
  1865.    else {
  1866.        warn "Error: $res->code\n", $res->content;
  1867.        return;
  1868.    }
  1869. }
  1870.  
  1871. sub _prepare_request {
  1872.    my ($req, $length) = @_;
  1873.    $req->header('GData-Version' => 2);
  1874.    $req->header('Content-Length' => $length) if ($length);
  1875.  
  1876.    if ($lwp_header{Authorization}) {
  1877.        $req->header(Authorization => $lwp_header{Authorization});
  1878.        $req->header('X-GData-Key' => $lwp_header{'X-GData-Key'});
  1879.    }
  1880.    else {
  1881.        warn $prompt{needs_login};
  1882.    }
  1883. }
  1884.  
  1885. sub _save {
  1886.    my ($method, $uri, $content) = @_;
  1887.  
  1888.    my $req = HTTP::Request->new("$method" => $uri);
  1889.    $req->content_type('application/atom+xml; charset=UTF-8');
  1890.    _prepare_request($req, length($content));
  1891.    $req->content($content);
  1892.  
  1893.    return _request($req);
  1894. }
  1895.  
  1896. sub show_comments {
  1897.    my ($code, $index) = @_;
  1898.  
  1899.    print "\n";
  1900.    $index ||= 1;
  1901.    my $hash = xml2hash(
  1902.                        lwp_get(
  1903.                                "$CONFIG{feeds_main_url}/videos/$code/comments?"
  1904.                                  . array_to_gdata_arguments(
  1905.                                                             'v'           => $constant{gdata_version},
  1906.                                                             'start-index' => $index
  1907.                                                            )
  1908.                               )
  1909.                       );
  1910.  
  1911.    my $number = 0;
  1912.    while (
  1913.           my $gdata =
  1914.             ref $hash->{feed}{entry} eq 'ARRAY' ? $hash->{feed}{entry}[$number]
  1915.           : ref $hash->{feed}{entry} eq 'HASH'  ? $hash->{feed}{entry}
  1916.           : $hash->{entry}
  1917.      ) {
  1918.        last unless defined $gdata;
  1919.        printf("$c{bold}%s$c{reset} on %s said:\n\t%s\n\n",
  1920.               $gdata->{author}{name},
  1921.               format_date($gdata->{updated}),
  1922.               $gdata->{content});
  1923.        ++$number;
  1924.        last unless ref $hash->{feed}{entry} eq 'ARRAY';
  1925.    }
  1926.  
  1927.    if ($number == 0) {
  1928.        print "No comments!\n\n";
  1929.        print_results();
  1930.    }
  1931.  
  1932.    {
  1933.        given ($term->readline($prompt{comments_next_page})) {
  1934.            when (q{}) {
  1935.                show_comments($code, $index + 25);
  1936.            }
  1937.            when (['?', 'help', 'h']) {
  1938.                print "\n", <<"HELP";
  1939. <ENTER>     : next page of comments
  1940. c, comment  : sent a comment to this video
  1941. d, done     : return to video results
  1942. ?, h, help  : this message
  1943. HELP
  1944.                redo;
  1945.            }
  1946.            when (['c', 'comment']) {
  1947.                print "\n$c{bold}=>> $c{bgreen}Write your comment here - press CTRL+D when you are done$c{reset}\n";
  1948.                chomp(my $comment = join(q{}, <STDIN>));
  1949.                $comment =~ s/[^\s[:^cntrl:]]+//g;
  1950.                send_comment_to_video($code, $comment);
  1951.                redo;
  1952.            }
  1953.            default {
  1954.                print "\n";
  1955.                print_results();
  1956.            }
  1957.        }
  1958.    }
  1959.    return 1;
  1960. }
  1961.  
  1962. sub main_quit {
  1963.    write_config_to_file() if $opt{update_config};
  1964.    exit 0;
  1965. }
  1966.  
  1967. main_quit();
  1968.  
  1969. package Config;
  1970.  
  1971. sub _dump {
  1972.    require Data::Dumper;
  1973.    return Data::Dumper::Dumper(shift);
  1974. }
  1975.  
  1976. sub _sort_items {
  1977.    my ($data) = @_;
  1978.    my ($items) = $data =~ /\{(.+?)\s*\};?\s*\z/s;
  1979.    $items .= ',';
  1980.    $data = "#!/usr/bin/perl\n\nscalar {"
  1981.       . join(
  1982.              "\n",
  1983.              (
  1984.               sort { lc $a cmp lc $b }
  1985.                 split(/\n/, $items, 0)
  1986.              )
  1987.             ) . "\n};\n";
  1988.  
  1989.     $data =~ s{=>\s*'(\d+)',\s*$}  {=> $1,}gm;
  1990.     $data =~ s{(.+?)\s*=>\s*(.+)}{
  1991.         sprintf '%s%*s', $1, 45 - length($1) + length($2), ' => ' . $2;
  1992.     }egm;
  1993.     return $data;
  1994. }
  1995.  
  1996. sub save_hash {
  1997.     my ($file, $config) = @_;
  1998.     return unless ref $config eq 'HASH';
  1999.     open(my $fh, '>', $file) or return;
  2000.     print {$fh} _sort_items(_dump($config));
  2001.     close $fh;
  2002.     return 1;
  2003. }
  2004.  
  2005. 1;
RAW Paste Data