#!/usr/bin/perl # # Copyright (C) 2010-2012 Trizen . # # 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 . # #------------------------------------------------------- # Appname: youtube-viewer # Created on: 02 June 2010 # Latest edit on: 09 May 2012 # Website: http://trizen.googlecode.com #------------------------------------------------------- # # [?] What is this script for? # - This script is useful to search and watch YouTube videos with MPlayer... # - Have fun! # # [!] The most important changes are written in the changelog! # # [CHANGELOG] # - 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 ) - 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 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 bugs reports and for his great ideas) # - dhn (for adding youtube-viewer in freshports.org) # - stressat (for the great review of youtube-viewer: http://stressat.blogspot.com/2012/01/youtube-viewer.html) # - symbianflo (for packaging youtube-viewer for Mandriva distribution) # - gotbletu (for the great video review of youtube-viewer: http://www.youtube.com/watch?v=FnJ67oAxVQ4) # - Julian Ospald for adding youtube-viewer in gentoo portage tree eval 'exec perl -S $0 ${1+"$@"}' if 0; # not running under some shell use 5.010; use strict; use autouse 'XML::Fast' => qw(xml2hash); use autouse 'URI::Escape' => qw(uri_escape); use File::Spec::Functions qw(catdir curdir path rel2abs tmpdir); #-------FOR DEBUG ONLY-------# # use diagnostics -v; # use warnings FATAL => 'all'; #----------------------------# my $appname = 'Youtube Viewer'; my $version = '2.5.9'; my $execname = 'youtube-viewer'; # Configuration dir/file my $config_dir = ( exists $ENV{XDG_CONFIG_HOME} ? $ENV{XDG_CONFIG_HOME} : ( $ENV{HOME} || $ENV{LOGDIR} || (getpwuid($<))[7] || `echo -n ~`) . '/.config' ) . "/$execname"; my $config_file = "$config_dir/$execname.conf"; my $noconfig = qr/^--?(?>N|noconfig)$/ ~~ @ARGV; # A better support: require Term::ReadLine; my $term = Term::ReadLine->new("$appname $version"); # Unchangeable variables goes here my %constant = ( gdata_version => 2, dash_line => q{-} x 80, win32 => $^O =~ /win|dos/i || 0, ); # Set $PATH to @path my @path = path(); # Locating gcap my $gcap; foreach my $path (@path, @INC) { if (-e (my $gcap_path = catdir($path, 'gcap'))) { $gcap = $gcap_path; last; } } # Get mplayer line sub get_mplayer { if ($constant{win32}) { my $smplayer = $ENV{ProgramFiles} . '\\SMPlayer\\mplayer\\mplayer.exe'; if (-e $smplayer) { return $smplayer; # Windows MPlayer } else { warn "\n\n!!! Please install SMPlayer in order to stream YouTube videos.\n\n"; return 'mplayer.exe'; } } else { my $mplayer_path = '/usr/bin/mplayer'; return -x $mplayer_path ? $mplayer_path : q{mplayer} # *NIX MPlayer } } # Main configuration my %CONFIG = ( # MPlayer options cache => 30000, cache_min => 5, lower_cache => 2000, lower_cache_min => 3, mplayer => get_mplayer(), mplayer_srt_settings => '-unicode -utf8', mplayer_arguments => '-prefer-ipv4 -really-quiet -cache %d -cache-min %d', # Youtube options results => 20, resolution => 1080, only_hd_videos => undef, safe_search => undef, only_videos_with_caption => undef, duration => undef, query_parameters => undef, time_sort => 'all_time', order_by => 'relevance', # URI options youtube_video_url => 'http://www.youtube.com/watch?v=%s', feeds_main_url => 'http://gdata.youtube.com/feeds/api', get_video_info => 'http://www.youtube.com/get_video_info', google_client_login => 'https://www.google.com/accounts/ClientLogin', # Subtitle options srt_language => 'en', tmp_dir => tmpdir(), gcap => $gcap, # Others use_colors => 0, lwp_downloading => 0, fullscreen => 0, use_lower_cache => 0, results_with_details => 0, perl_bin => $^X, thousand_separator => q{,}, downloads_folder => curdir(), editor => $ENV{EDITOR} || 'nano', users_list => "$config_dir/youtube_users.txt", ); my $stdin_help = <<'STDIN_HELP'; all : play all the results in order next : go to the next page (same as ) back : return to the previous page login : will prompt you for login logout : will delete the authentication key [integer] : play the corresponding video i, info [i] : show more informations about one video c, comments [i] : show video comments (e.g.: c 19) r, related [i] : show related videos (e.g.: r 6) v, videos [i] : show author's latest videos p, playlists [i] : show author's latest playlists subscribe [i] : subscribe to author's channel like, dislike [i] : like or dislike a video fav, favorite [i] : favorite a video (e.g.: fav 3) [keywords] : search for youtube videos 3-8, 3..8 : same as 3 4 5 6 7 8 8 2 12 4 6 5 1 : play the videos in your order -argv -argv2=v : set some arguments (e.g.: -u=google) e, edit-config : edit and apply the configuration load-config : (re)load the configuration file /my?[regex]*$/ : play videos matched by a regex (/i) reset, reload : restart the application q, quit, exit : close the application STDIN_HELP my %MPLAYER; # MPlayer variable arguments sub set_mplayer_arguments { my ($cache, $cache_min) = @_; $MPLAYER{mplayer_arguments} = sprintf $CONFIG{mplayer_arguments}, $cache, $cache_min; $MPLAYER{fullscreen} = $CONFIG{fullscreen} ? q{-fs} : q{}; return 1; } # Save hash config to file sub write_config_to_file { Config::save_hash($config_file, \%CONFIG); } # Creating config unless it exists unless (-e $config_file) { require File::Path; File::Path::make_path($config_dir); write_config_to_file(); } # itag => resolution my %itags = ( 37 => 1080, 22 => 720, 35 => 480, 43 => 210, 34 => 360, 5 => 240 ); # For YouTube my $key = 'AI39si5xZtotT-ABtXEHNYpPnfez4T9hfNTkMlWti5gVCbFOZ-wyw70RTTguH_53klpmj3G98sTJGXJF5YY61Zcu1r5XmR2w3Q'; my %lwp_header = ('X-GData-Key' => "key=$key"); #----------------------- COLORS -----------------------# my %c = ( bold => q{}, bred => q{}, bgreen => q{}, reset => q{}, cblack => q{}, byellow => q{}, bpurle => q{}, bblue => q{}, ); if (not $constant{win32}) { # if not running under Windows $c{cblack} = "\e[40m"; # background black $c{byellow} = "\e[1;33m"; # bold yellow $c{bpurle} = "\e[1;35m"; # bold purple $c{bblue} = "\e[1;34m"; # bold blue $c{bold} = "\e[1m"; # bold terminal color $c{bred} = "\e[1;31m"; # bold red $c{bgreen} = "$c{cblack}\e[1;32m"; # bold green on black background $c{reset} = "\e[0m"; # reset color } if (qr/^--?nocolors$/ ~~ \@ARGV) { # --nocolors %c = map { $_ => q{} } keys %c; } #---------------------- GLOBAL VARIABLES ----------------------# my $keywords = q{}; # used to store keywords for search my $youtube_gdata_url; # used to store the gdata URL with API query parameters # Other global variables my ($lwp, @picks, @results, %streaming); my %prompt = ( comments_next_page => "$c{reset}\n$c{bold}=>> $c{bgreen}Press for the next page of comments (? - help)$c{reset}\n> ", select_video_to_play => "$c{reset}\n$c{bold}=>> $c{bgreen}Insert a number or search something else (? - help)$c{reset}\n> ", init_text => "$c{reset}\n$c{bold}=>> $c{bgreen}Insert an YouTube URL or search something...$c{reset}\n> ", email => "$c{reset}\n$c{bold}=>> $c{bgreen}Email:$c{reset} ", password => "$c{reset}$c{bold}=>> $c{bgreen}Password:$c{reset} ", user_from_list => "$c{reset}\n$c{bold}=>> $c{bgreen}Pick one username$c{reset}\n> ", video_playlists => "$c{reset}\n$c{bold}=>> $c{bgreen}Pick one playlist or search something else$c{reset}\n> ", user_playlists => "$c{reset}\n$c{bold}=>> $c{bgreen}Pick one playlist or insert another username$c{reset}\n> ", categories => "$c{reset}\n$c{bold}=>> $c{bgreen}Pick one category$c{reset}\n> ", needs_login => "$c{bred}You need to login in order to use this feature!$c{reset}\n", ); # Options my %opt = (start_with_page => 1); #------------------------ REGEXP AREA ------------------------# my $contains_arguments = qr/(?>\s|^)-+\w/; my $match_regexp = qr{/([^\\/]*(?:\\.[^\\/]*)*)/}; my $valid_playlist_code = qr/^(?:PL)?([0-9A-Z]{16})$/; my $get_youtube_code = qr{\b(?>v|embed|youtu[.]be)(?>[=/]|%3D)([\w\-]{11})}; my $looks_like_gdata_url = qr{^https?://gdata[.]youtube[.]com/feeds/.}; # $1 will be the playlist code my $get_playlist_code = qr{(?:(?:(?>playlist[?]list|view_play_list[?]p)=)|\w#p/c/)(?:PL)?([A-Z0-9]{16})}; # $1 will be the Youtube username my $get_username = qr{^https?://(?:www[.])?youtube[.]com/(?:user/)?(\w{2,})(?:[?].*)?$}; # The bellow regex will validate an HTTP url. # If it is valid, we will try to get an youtube # video code from that website using /$get_youtube_code/ my $valid_url = qr{^ ################# This regex will validate an HTTP URL ################# https?:// # http or https followed by :// [[:alnum:]] # first character must be a-zA-Z0-9 (?:(?:(?:\w*-+\w+|\w+)* # words, dash, words OR only words \.(?=\w))+? # point if followed by word char | # OR (validates http://x.yz) \.) # a single dot \w{2,6} # domain (words between 2 and 6 chars) (?:[#/?!] # characters after domain [#-)+-;=?\\~\w]*)* # the rest characters of the string $}x; #---------------------- LOOKING FOR WGET ----------------------# sub locate_wget { foreach my $dir (@path) { if (-x (my $wget_path = catdir($dir, 'wget'))) { $opt{wget} = $wget_path; return 1; } } return; } #---------------------- LWP::UserAgent ----------------------# sub set_lwp_useragent { require LWP::UserAgent; $lwp = 'LWP::UserAgent'->new( keep_alive => 1, env_proxy => 1, timeout => 60, show_progress => $opt{debug} ? 1 : 0, agent => 'Mozilla/5.0 (X11; U; Linux i686; en-US) Chrome/10.0.648.45', ); $opt{lwp_is_set} = 1; binmode *STDOUT, ":encoding(UTF-8)"; } sub lwp_get { set_lwp_useragent() unless $opt{lwp_is_set}; return $lwp->get($_[0], %lwp_header)->content; } # True if looks like a YouTube code # XXX: it may not validate any code sub is_code { length $_[0] == 11 and $_[0] =~ /^[\w-]{11}$/ # must contain only word chars and dashes and $_[0] =~ /[0-9A-Z_\-]/ # must contain, at least, one 'unusual' char and not $_[0] =~ /^--?[a-z]+$/; # and is not an argument (e.g.: -fullscreen) } # Set config file to %CONFIG sub apply_configuration { my $config_ref = do $config_file; if (ref $config_ref eq 'HASH') { while (my ($key, $value) = each %$config_ref) { $CONFIG{$key} = $value; } } else { warn "\n[!] Configuration file contains some errors!\n", "[*] Trying to regenerate the configuration file...\n", (write_config_to_file() ? "[*] Done!\n" : "[!] Unable to regenerate $config_file: $!\n"); } # Auto-login if (defined $CONFIG{auth}) { set_auth_key($CONFIG{auth}); } } apply_configuration() unless $noconfig; # Convert args (of length of 11) to Youtube URLs { my @argv; foreach my $arg (@ARGV) { if (is_code($arg)) { push @argv, sprintf($CONFIG{youtube_video_url}, $arg); } else { push @argv, $arg; } } @ARGV = @argv; } # __DIE__ handle $SIG{__DIE__} = sub { if (join(q{}, @_) =~ m{^Can't locate (.+?)\.pm\b}) { #' my $module = $1; $module =~ s{[/\\]+}{::}g; die <<"REQ"; Module $module is required!\n To install it, just type in terminal: sudo cpan -i $module REQ } return 1; }; sub require_getopt_long { require Getopt::Long; Getopt::Long::Configure('no_ignore_case'); $opt{required_getopt} = 1; } # Parsing arguments if (@ARGV) { parse_arguments(@ARGV); } # Return value for start_index sub get_start_index { return $CONFIG{results} * $opt{start_with_page} - $CONFIG{results} + 1; } # Returns a list of arguments sub get_arguments_from_string { return grep { chr ord eq q{-} } split(q{ }, shift); } # Returns a list of keywords sub get_keywords_from_string { return grep { chr ord ne q{-} } split(q{ }, shift); } # Returns a list of keywords sub get_keywords_from_array { return grep { chr ord ne q{-} } ref $_[0] eq 'ARRAY' ? @{$_[0]} : @_; } sub parse_arguments { require_getopt_long() unless $opt{required_getopt}; Getopt::Long::GetOptionsFromArray( \@_, \%CONFIG, # Main options 'help|usage|h|?' => \&help, 'tricks|tips|T' => \&tricks, 'version|V|v' => \&version, # Resolutions '240p|2' => sub { $CONFIG{resolution} = 240 }, '360p|3' => sub { $CONFIG{resolution} = 360 }, '480p|4' => sub { $CONFIG{resolution} = 480 }, '720p|7' => sub { $CONFIG{resolution} = 720 }, '1080p|1' => sub { $CONFIG{resolution} = 1080 }, # Other options 'categories|c' => sub { push @{$opt{subs}}, [\&categories_area] }, 'movies|M' => sub { push @{$opt{subs}}, [\&youtube_movies] }, 'today|t' => sub { push @{$opt{subs}}, [\&youtube_tops] }, 'a|all-time' => sub { push @{$opt{subs}}, [\&youtube_tops, 'all_time'] }, 'subscriptions|S:s' => sub { push @{$opt{subs}}, [\&get_new_subsc, $_[-1] ? pop() : 'default'] }, 'favorites|favorited|F:s' => sub { push @{$opt{subs}}, [\&get_favorited_videos, $_[-1] ? pop() : 'default'] }, 'recommended|R:s' => sub { push @{$opt{subs}}, [\&get_recommended_videos, $_[-1] ? pop() : 'default'] }, 'watched|W:s' => sub { push @{$opt{subs}}, [\&get_watch_history, $_[-1] ? pop() : 'default'] }, 'login' => sub { push @{$opt{subs}}, [\&authenticate] }, 'user|u=s' => sub { push @{$opt{subs}}, [\&videos_from_username, pop] }, 'user-playlists|up=s' => sub { push @{$opt{subs}}, [\&playlists_from_username, pop] }, 'users:s' => sub { push @{$opt{subs}}, [\&list_user_names => $_[-1] ? pop() : $CONFIG{users_list}] }, 'author=s' => \$opt{author}, 'all|A!' => \$opt{playback}, 'nocolors' => \$opt{no_colors}, 'nostdin' => \$opt{no_stdin}, 'noconfig|N!' => \$noconfig, 'playlists|p!' => \$opt{playlists}, 'debug!' => \$opt{debug}, 'update-config|U!' => \$opt{update_config}, 'download|d!' => \$opt{download_video}, 'print_video_id' => \$opt{print_video_id}, 'safe-search=s' => \$CONFIG{safe_search}, 'hd!' => \$CONFIG{only_hd_videos}, 'cache-min=i' => \$CONFIG{cache_min}, 'colors|C' => \$CONFIG{use_colors}, 'details|D!' => \$CONFIG{results_with_details}, 'caption=s' => \$CONFIG{only_videos_with_caption}, 'sub|lang=s' => \$CONFIG{srt_language}, 'fs|f!' => \$CONFIG{fullscreen}, 'l|lower-cache!' => \$CONFIG{use_lower_cache}, 'query-params|Q=s' => \$CONFIG{query_parameters}, 'lwp-download|L!' => \$CONFIG{lwp_downloading}, 'order-by=s' => \$CONFIG{order_by}, 'time=s' => \$CONFIG{time_sort}, 'page=i' => sub { $opt{start_with_page} = pop }, 'append_mplayer=s' => sub { $MPLAYER{argv} = pop }, 'novideo|n!' => sub { $MPLAYER{novideo} = $_[-1] ? q{-novideo} : q{} }, 'more|m!' => sub { $CONFIG{results} = $_[-1] ? 50 : 20 }, 'download-dir|downloads-dir=s' => \$CONFIG{downloads_folder}, 'quiet|q' => sub { close STDOUT; close STDERR; }, map { defined $CONFIG{$_} && $CONFIG{$_} =~ /^[01]\z/ ? "$_!" : "$_=s" } keys %CONFIG ); $keywords = join(q{ }, get_keywords_from_array(\@_)) if @_; # Start with page N $opt{start_index} = get_start_index(); if ($opt{no_stdin}) { # poor implementation of --nostdin $term = q{}; $SIG{__DIE__} = sub { exit 0 }; close STDERR; } # Dump config if ($opt{debug}) { print "=>> CONFIG:\n" => Config::_dump(\%CONFIG), "=>> MPLAYER:\n" => Config::_dump(\%MPLAYER); } # Go to selected subroutines if (exists $opt{subs}) { while (@{$opt{subs}}) { my $goto = shift @{$opt{subs}}; my $sub = shift @{$goto}; $sub->(@{$goto}); } } } $opt{start_index} ||= get_start_index() || 1; #---------------------- PARSING VIDEO CODES SPECIFIED AS ARGUMENTS ----------------------# foreach my $code (@ARGV) { given ($code) { when (/$get_playlist_code/o) { list_playlist($1); } when (/$valid_playlist_code/o) { list_playlist($1); } when (/$get_youtube_code/o) { $opt{video_id_from_arguments} = 1; get_youtube($1); } when (/$get_username/o) { videos_from_username($1); } when (/$valid_url/o) { code_from_content($_); } } } sub quit_required { $_[0] ~~ ['q', 'quit', 'exit']; } #---------------------- GO TO insert_url() IF $non_argv is FALSE ----------------------# sub insert_url { { given ($term->readline($prompt{init_text})) { when (\&check_user_input) { redo; } when (q{}) { die "\n$c{bred}(x_x) Unable to continue...$c{reset}\n\n"; } default { search(join(q{ }, get_keywords_from_string($_)) or redo); } } } } insert_url() unless length $keywords; #---------------------- GET A VIDEO CODE FROM AN WEBSITE CONTENT ----------------------# sub code_from_content { set_lwp_useragent() unless $opt{lwp_is_set}; if ($lwp->get($_[0])->content =~ /$get_youtube_code/o) { get_youtube($1); } else { search($_[0]); } } #---------------------- YOUTUBE-VIEWER USAGE ----------------------# sub help { my $eqs = q{=} x 30; print <<"HELP"; \n $eqs $c{bgreen}\U$appname\E$c{reset} $eqs \t\t\t\t\t\t by Trizen (trizenx\@gmail.com) \n$c{bold}usage:$c{reset} $execname [options] ([code] || [url] || [keywords]) \n$c{bgreen}Base Options:$c{reset} : play an YouTube video by URL : play an YouTube video by code : search and list YouTube videos : list a playlist of YouTube videos \n$c{bgreen}YouTube options:$c{reset} -t --today : show YouTube tops of today -a --all-time : show YouTube tops of all time -c --categories : show available YouTube categories -p --playlists : search for playlists of videos -M --movies : show YouTube category of movies -results=[1-50] : how many videos to display per page -u=s -user=s : list videos uploaded by a specific user -up=s : list playlists created by a specific user -author=s : search videos uploaded by a particular user -duration=s : valid values are: short, medium, long -caption=s : valid values are: true, false -safe-search=s : valid values are: none, moderate, strict -order-by=s : order entries by: published, viewCount and rating -time=s : valid values are: today, this_week and this_month -page=i : show video results starting with the page 'i' -hd : show only the videos available in at least 720p -2 -3 -4 -7 -1 : resolution of videos: 240p, 360p, 480p, 720p or 1080p -F --favorites : show the latest favorited videos * -R --recommended : show the recommended videos for you * -S --subscriptions : show the new subscription videos * -W --watched : show the latest watched videos on YouTube * \n$c{bgreen}MPlayer options:$c{reset} -f --fullscreen : set the fullscreen mode for mplayer (-fs) -n --novideo : play the music only without a video in the foreground -l --lower-cache : use a lower cache for MPlayer (for slow connections) -sub=s -lang=s : subtitle language (default: en) (depends on gcap) -cache=i : set the cache for MPlayer (set: $CONFIG{cache}) -cache-min=i : set the cache-min for MPlayer (set: $CONFIG{cache_min}) -mplayer=s : set a media player (set: $CONFIG{mplayer}) -mplayer_arguments=s : replace default arguments for the media player -append_mplayer=s : add some arguments for the media player \n$c{bgreen}Other options:$c{reset} -d --download : download the video(s) -A --all : play all the video results in order -C --colors : use colors to delimit the video results -D --details : a new look for the results, with more details -L --lwp-download : download the videos with LWP (default: wget) -T --tricks : show more 'hidden' features of $appname -U --update-config : update the configuration file before exit -N --noconfig : start the $appname with the default config -Q --query-params : set query parameters (e.g.: 'duration=long&caption') -users=file.txt : list YouTube usernames from a file -login : will prompt you for login -downloads-dir : downloads directory (set: '$CONFIG{downloads_folder}') \n$c{bgreen}Main options:$c{reset} -q --quiet : display no output -v --version : print version and exit -h --help : print help and exit $c{bold}NOTE:$c{reset} * == requires authentication -no-[argv] will negate the value of the argument (e.g.: -no-fullscreen) each config key is a valid argument (if it's preceded by a dash (-)) $c{bgreen}Tips and tricks:$c{reset} 1. After the search results, press for the next page 2. After the search results, insert 'back' for the previous page 3. View more 'hidden' features by executing '$execname -T'\n HELP main_quit(); } #---------------------- YOUTUBE-VIEWER TIPS AND TRICKS ----------------------# sub tricks { print <<"TRICKS"; $c{bold}>>$c{reset} $c{bgreen}Tips and tricks$c{reset} $c{bold}<<$c{reset} \n$c{bold}* $c{bgreen}STDIN arguments:$c{reset} $stdin_help \n$c{bold}* $c{bgreen}Did you know that...?$c{reset} -A option will play ALL video results, including videos from the next pages;\n /REGEXP/ will match case-insensitive (e.g.: /test/ matches 'TeSt');\n When multiple videos are selected to play, pressing CTRL+C will just empty the playlist and return to the video results. \n$c{bold}* $c{bgreen}Configuration file$c{reset} Since 2.5.5 version, $appname supports a configuration file. Config file is: '$config_file' \n$c{bold}* $c{bgreen}Usage examples:$c{reset} ** Show videos uploaded by 'MIT' that matches 'computer science', starting with page number 2, in fullscreen mode and 720p resolution. % $execname --author=MIT computer science --page=2 -fs --720p\n ** Show playlists created by a specific user % $execname -up khanacademy\n ** Show latest videos (50) uploaded by a specific user and a colorful output % $execname -results=50 -u google -C\n TRICKS main_quit(); } # Print version sub version { print "Youtube Viewer $version\n"; main_quit(); } # ------------------ Authentication ------------------ # sub set_auth_key { my ($auth) = @_; $lwp_header{Authorization} = "GoogleLogin auth=$auth"; } sub log_out { delete $lwp_header{Authorization}; set_lwp_useragent(); } sub authenticate { my ($email, $password); $email = $term->readline($prompt{email}); if (defined $CONFIG{password}) { $password = $CONFIG{password}; } else { if ($constant{win32}) { eval { require Term::ReadKey }; if ($@) { say "[!] Please install Term::ReadKey if you don't want your password to be visible while typing!"; $password = $term->readline($prompt{password}); } else { $password = q{}; print $prompt{password}; Term::ReadKey::ReadMode('noecho'); while (ord(my $key = Term::ReadKey::ReadKey(0)) != 10) { $password .= $key; } Term::ReadKey::ReadMode('restore'); } } else { print $prompt{password}; system('stty', '-echo'); chomp($password = ); system('stty', 'echo'); } } print "\n\n$c{bold}**$c{reset} Should I save your authentification key into configuration?\n", "-> if 'yes', you will be logged automatically next time\n\n", "$c{bold}=>>$c{reset} Your answer [y/N]: "; my $remember_me = =~ /^y(?:es)?$/i ? 1 : 0; set_lwp_useragent() unless $opt{lwp_is_set}; my $resp = $lwp->post( $CONFIG{google_client_login}, [Content => 'application/x-www-form-urlencoded', Email => $email, Passwd => $password, service => 'youtube', source => "$appname $version" ] ); if ($resp->{_content} =~ /^Auth=(.+)/m) { my $auth = $1; if ($remember_me) { $CONFIG{auth} = $auth; $opt{update_config} = 1; } say "\n$c{bold}* $c{bgreen}Logged!$c{reset}"; set_auth_key($auth); } else { warn "\nUnable to login: $resp->{_content}\n"; return 0; } return 1; } sub get_new_subsc { my ($user) = @_; unless ($lwp_header{Authorization}) { warn $prompt{needs_login}; authenticate(); } parse_url("$CONFIG{feeds_main_url}/users/$user/newsubscriptionvideos"); } sub get_recommended_videos { my ($user) = @_; unless ($lwp_header{Authorization}) { warn $prompt{needs_login}; authenticate(); } parse_url("$CONFIG{feeds_main_url}/users/$user/recommendations"); } sub get_favorited_videos { my ($user) = @_; unless ($lwp_header{Authorization}) { warn $prompt{needs_login}; authenticate(); } parse_url("$CONFIG{feeds_main_url}/users/$user/favorites"); } sub get_watch_history { my ($user) = @_; unless ($lwp_header{Authorization}) { warn $prompt{needs_login}; authenticate(); } parse_url("$CONFIG{feeds_main_url}/users/$user/watch_history"); } #---------------------- LIST YOUTUBE USERNAMES FROM A FILE ----------------------# sub list_user_names { my ($users_file) = @_; return unless -T $users_file; my $i = 0; my %usernames_table; print "\n"; open my $fh, '<:crlf', $users_file or die $!; while (defined(my $username = <$fh>)) { next unless $username =~ /^\w+$/; chomp $username; printf "%s%2d%s - %s%s%s\n", $c{bold}, ++$i, $c{reset}, $c{bgreen}, $username, $c{reset}; $usernames_table{$i} = $username; } close $fh; { given ($term->readline($prompt{user_from_list})) { when (\&check_user_input) { redo; } when (exists $usernames_table{$_}) { videos_from_username($usernames_table{$_}); } when (/^\w+$/) { videos_from_username($_); } when (/$match_regexp/o) { my $match = qr/$1/i; my ($found, @found) = 0; print "\n"; while (my ($number, $username) = each %usernames_table) { if ($username =~ /$match/o) { printf "%s%2d%s - %s%s%s\n", $c{bold}, ++$found, $c{reset}, $c{bgreen}, $username, $c{reset}; push @found, $found; $usernames_table{$found} = $username; } } if (@found > 1) { given ($term->readline($prompt{user_from_list})) { when (\&quit_required) { main_quit(); } when (exists $usernames_table{$_}) { videos_from_username($usernames_table{$_}); } default { insert_url(); } } } elsif (@found) { videos_from_username($usernames_table{$found[0]}); } continue; } default { insert_url(); } } } } #---------------------- GET VIDEOS FROM A SPECIFIC USER ----------------------# sub videos_from_username { parse_url("$CONFIG{feeds_main_url}/users/$_[0]/uploads"); } #---------------------- PRINT PLAYLISTS ----------------------# sub print_playlists { my ($playlist, $num) = @_; # Number, Title, Author, VideosCount $CONFIG{use_colors} # Colorful ? printf( "%s%s%2d%s%s - %s%s%s%s (%sby %s%s%s) (%s%s%s%s)%s\n", $c{cblack} => $c{bold} => $num => $c{reset}, $c{cblack} => $c{byellow} => $playlist->{title} => $c{reset}, $c{cblack} => $c{bpurle} => $playlist->{author} => $c{reset}, $c{cblack} => $c{bblue} => $playlist->{count} => $c{reset}, $c{cblack} => $c{reset} ) : printf("%s%2d%s - %s (by %s) (%s)\n", $c{bold}, $num, $c{reset}, $playlist->{title}, $playlist->{author}, $playlist->{count}); } #---------------------- GET PLAYLISTS FROM A SPECIFIC USER ----------------------# my $playlist_index; sub playlists_from_username { my ($username) = @_; search_playlists("$CONFIG{feeds_main_url}/users/$username/playlists?" . default_gdata_arguments(), 'username'); } sub search_playlists { return unless @_; my ($arg, $mode) = @_; $youtube_gdata_url = $arg =~ /$looks_like_gdata_url/o ? $arg : $CONFIG{feeds_main_url} . "/playlists/snippets?q=$arg&" . default_gdata_arguments(); $opt{playlists} = 1; parse_content($youtube_gdata_url); my $i = 0; foreach my $playlist (@results) { print_playlists($playlist, ++$i); } playlists_waiting_input($mode); } #---------------------- SEARCH FOR YOUTUBE PLAYLISTS ----------------------# sub playlists_waiting_input { my ($mode) = @_; { my $prompt = defined $mode && $mode eq 'playlists' ? $prompt{video_playlists} : $prompt{user_playlists}; given ($term->readline($prompt)) { when (\&check_user_input) { redo; } when ([qr/^\s*$/, 'next']) { next_page($mode); } when ('back') { if ( do { $youtube_gdata_url =~ /[&?]start-index=(\d+)/; $1 > $CONFIG{results}; } ) { previous_page($mode); } else { continue; } } when (/^\d+$/ and $_ > 0 and $_ <= @results) { list_playlist($results[$_ - 1]{playlistID}); } default { if ($mode eq 'playlists') { search($_); } elsif ($mode eq 'username') { playlists_from_username($_); } else { print_results(); } } } } } #---------------------- LIST A YOUTUBE PLAYLIST ----------------------# sub list_playlist { $opt{playlists} = 0; parse_url("$CONFIG{feeds_main_url}/playlists/$_[0]"); } #---------------------- LIST YOUTUBE MOVIE CATEGORIES ----------------------# sub youtube_movies { print "\n"; my $i = 0; my %movie_table; foreach my $movie_cat_name ('most_popular', 'most_recent', 'trending') { my $cat_name = ucfirst $movie_cat_name; $cat_name =~ tr/_/ /; print q{ }, $c{bold}, ++$i, "$c{reset} - $cat_name\n"; $movie_table{$i} = $movie_cat_name; } { given ($term->readline($prompt{categories})) { when (\&check_user_input) { redo; } when (exists $movie_table{$_}) { parse_url("$CONFIG{feeds_main_url}/charts/movies/$movie_table{$_}"); } } } } #---------------------- LIST YOUTUBE TOP VIDEO CATEGORIES ----------------------# sub youtube_tops { my $i = 0; my $today = $_[0] && $_[0] eq 'all_time' ? 0 : 1; my $standardfeeds = "$CONFIG{feeds_main_url}/standardfeeds"; my %tops_table; print "\n"; foreach my $cat_top_name ( 'top_rated', 'top_favorites', 'most_viewed', 'most_popular', 'most_recent', 'most_discussed', 'most_responded', 'recently_featured' ) { my $top_name = ucfirst $cat_top_name; $top_name =~ tr/_/ /; print q{ }, $c{bold}, ++$i, "$c{reset} - $top_name\n"; $tops_table{$i} = $cat_top_name; } { given ($term->readline($prompt{categories})) { when (\&check_user_input) { redo; } when (exists $tops_table{$_}) { my $url = "$standardfeeds/$tops_table{$_}"; if ($today and not $url =~ /recent/) { $url .= '?time=today'; } parse_url($url); } default { insert_url(); } } } } #---------------------- LIST YOUTUBE VIDEO CATEGORIES ----------------------# sub categories_area { my $n = 0; my %categories_table; print "\n"; foreach my $cat (@{xml2hash(lwp_get('http://gdata.youtube.com/schemas/2007/categories.cat'))->{'app:categories'}{'atom:category'}}) { next if exists $cat->{'yt:deprecated'}; printf "%s%2d%s - %s\n", $c{bold}, ++$n, $c{reset}, $cat->{'-label'}; $categories_table{$n} = $cat->{'-term'}; } { given ($term->readline($prompt{categories})) { when (\&check_user_input) { redo; } when (exists $categories_table{$_}) { parse_url("$CONFIG{feeds_main_url}/videos?category=$categories_table{$_}"); } default { insert_url(); } } } } sub update_mplayer_arguments { if ( $CONFIG{use_lower_cache} or not exists $streaming{720} or $CONFIG{resolution} < 720) { set_mplayer_arguments($CONFIG{lower_cache}, $CONFIG{lower_cache_min}); } else { set_mplayer_arguments($CONFIG{cache}, $CONFIG{cache_min}); } } #---------------------- PLAY OR DOWNLOAD AN YOUTUBE VIDEO ----------------------# sub play_or_download { my $streaming = shift; print "** STREAMING: $streaming\n\n" if $opt{debug}; if ($opt{download_video}) { # DOWNLOADING my $title = shift @_; if (defined $CONFIG{downloads_folder}) { chdir $CONFIG{downloads_folder}; } if (not defined $opt{located_wget} and not $CONFIG{lwp_downloading}) { $opt{located_wget} = locate_wget() || -1; } # Replacing reserved characters with a space $title =~ tr[ "*/:<>?\\|][ ]s; if (not -e "$title.mp4") { if (defined $opt{wget}) { # Download video with wget system $opt{wget}, q{-nc}, $streaming, '-O', "$title.mp4"; } else { # Downloading video with LWP say "** Saving to: '$title.mp4'"; $lwp->show_progress(1); $lwp->mirror($streaming, "$title.mp4"); $lwp->show_progress(0) unless $opt{debug}; } } else { warn "** '$title.mp4' already exists...\n"; } } else { # STREAMING update_mplayer_arguments(); # Update mplayer's arguments my @mplayer_line = split(' ', join(' ', $CONFIG{mplayer}, values %MPLAYER)); if ($opt{debug}) { print "** MPlayer Line: @mplayer_line\n\n"; } else { system @mplayer_line, $streaming; } # Change directory back to the main working directory chdir delete $constant{cwd} if exists $constant{cwd}; } print "\n" unless $opt{video_id_from_arguments}; if ($?) { # if non-zero exit code $opt{playback} = 0; print_results(); } if (@picks) { foreach_pick(); # play the next video (if any) } elsif ($opt{playback}) { next_page(); } if (@results and not $opt{video_id_from_arguments}) { print_results(); # back to video results } main_quit() unless $opt{video_id_from_arguments}; return 1; } #---------------------- SEARCH YOUTUBE VIDEOS ----------------------# if (length($keywords)) { search() unless $opt{video_id_from_arguments}; } sub get_defined_pairs_from_array { # ('arg', undef, 'option', 'value') # --to-- # ('option', 'value') foreach (my $i = 0 ; $i <= $#_ ; ++$i) { if (not defined $_[$i]) { splice(@_, --$i, 2); } } return @_; } sub array_to_gdata_arguments { my @options = &get_defined_pairs_from_array; # ('arg', 'value', 'option', 'true') # --to-- # 'arg=value&option=true' my $i = -2; my $x = $#options - 1; my $str = q{}; while (1) { if ($i + 3 < $x) { $str .= $options[$i += 2] . '=' . $options[$i + 1] . '&'; } else { return $str .= $i + 3 == $x ? $options[-3] . '=' . $options[-2] . '&' . $options[-1] : $i + 2 == $x ? $options[-2] . '=' . $options[-1] : $options[-1]; } } } sub default_gdata_arguments { array_to_gdata_arguments( 'max-results' => $CONFIG{results}, 'start-index' => $opt{start_index}, 'v' => $constant{gdata_version}, ); } sub search { $keywords = shift() // $keywords; #/ # Get words which doesn't begins with a dash (-); $keywords = uri_escape(join(q{ }, get_keywords_from_string($keywords))); if ($opt{playlists}) { search_playlists($keywords, 'playlists'); return; } $youtube_gdata_url = "$CONFIG{feeds_main_url}/videos?" . array_to_gdata_arguments( 'q' => $keywords, 'max-results' => $CONFIG{results}, 'time' => $CONFIG{time_sort}, 'orderby' => $CONFIG{order_by}, 'start-index' => $opt{start_index}, 'safeSearch' => $CONFIG{safe_search}, 'hd' => $CONFIG{only_hd_videos} ? 'true' : undef, 'caption' => $CONFIG{only_videos_with_caption}, 'duration' => $CONFIG{duration}, 'author' => $opt{author}, 'v' => $constant{gdata_version} ); if (defined $CONFIG{query_parameters}) { unless ($CONFIG{query_parameters} =~ /^&/) { substr($CONFIG{query_parameters}, 0, 0, '&'); } $youtube_gdata_url .= $CONFIG{query_parameters}; } parse_content($youtube_gdata_url); print_results(); } #---------------------- PREPARE GDATA FEEDS URL ----------------------# sub parse_url { ($youtube_gdata_url) = @_; $youtube_gdata_url .= $youtube_gdata_url =~ /\?/ ? '&' : '?'; $youtube_gdata_url .= default_gdata_arguments(); parse_content($youtube_gdata_url); print_results(); } #---------------------- GET AND PARSE GDATA CONTENT ----------------------# sub parse_content { undef @results; my $number = 0; my $hash; eval { $hash = xml2hash(lwp_get($_[0])) }; if ($@) { if ($@ =~ /Module \S+ is required!/) { warn $@; main_quit(); } else { warn "Error ocurred while parsing $_[0]\n$@\n"; exit 1 if ++$opt{retry} == 16; parse_content($_[0]); } } while ( my $gdata = ref $hash->{feed}{entry} eq 'ARRAY' ? $hash->{feed}{entry}[$number] : ref $hash->{feed}{entry} eq 'HASH' ? $hash->{feed}{entry} : $hash->{entry} ) { last unless defined $gdata; push @results, $opt{playlists} # Playlists ? { 'playlistID' => $gdata->{'yt:playlistId'}, 'title' => $gdata->{title}, 'author' => $gdata->{author}{name}, 'count' => $gdata->{'yt:countHint'} } # Videos : { 'code' => $gdata->{'media:group'}{'yt:videoid'}, 'title' => $gdata->{'media:group'}{'media:title'}{'#text'}, 'author' => $gdata->{author}{name}, 'rating' => $gdata->{'gd:rating'}{'-average'} || 0, 'likes' => $gdata->{'yt:rating'}{'-numLikes'} || 0, 'dislikes' => $gdata->{'yt:rating'}{'-numDislikes'} || 0, 'favorited' => $gdata->{'yt:statistics'}{'-favoriteCount'}, 'duration' => format_time($gdata->{'media:group'}{'yt:duration'}{'-seconds'} || 0), 'views' => $gdata->{'yt:statistics'}{'-viewCount'}, 'published' => $gdata->{published}, 'description' => $gdata->{'media:group'}{'media:description'}{'#text'}, 'category' => ref $gdata->{category} eq 'ARRAY' ? $gdata->{category}[1]{'-label'} : $gdata->{category}{'-label'} || 'Unknown', }; ++$number; last unless ref $hash->{feed}{entry} eq 'ARRAY'; } if ($opt{debug}) { chdir($CONFIG{tmp_dir}); open my $fh, '>', "$execname.debug" or die "Unable to write to $execname.debug: $!\n"; local $, = "\n"; print {$fh} ( "=>> URL:" => $_[0], "=>> HASH:" => Config::_dump($hash), "=>> Results:" => Config::_dump(\@results) ); close $fh; } print "\n"; } # Format seconds to HH:MM:SS sub format_time { $_[0] >= 3600 ? join ':', map { sprintf '%02d', $_ } $_[0] / 3600 % 24, $_[0] / 60 % 60, $_[0] % 60 : join ':', map { sprintf '%02d', $_ } $_[0] / 60 % 60, $_[0] % 60; } sub check_user_input { given (shift) { when (\&quit_required) { main_quit(); } when (['e', 'edit-config']) { system $CONFIG{editor}, $config_file; apply_configuration; return 1; } when ('load-config') { apply_configuration(); return 1; } when ('login') { authenticate(); return 1; } when ('logout') { print "Logging out...\n"; log_out(); return 1; } when (/$contains_arguments/o) { parse_arguments(get_arguments_from_string($_)); continue; } when (['reset', 'reload']) { @ARGV = (); do $0; } when (/$get_youtube_code/o) { get_youtube($1); } when (/$get_playlist_code/o) { list_playlist($1); } when (/$valid_playlist_code/o) { list_playlist($_); } when (/$valid_url/o) { code_from_content($_); } when (\&is_code) { get_youtube($_); } } return; } #---------------------- PRINT VIDEO RESULTS ----------------------# sub print_results { unless (@results) { warn "$c{bred}(x_x) No video results!$c{reset}\n"; insert_url(); } my $num = 0; foreach my $video (@results) { print "$video->{code} - " if $opt{print_video_id}; if ($CONFIG{results_with_details}) { # Results with details (when using --details or -D) printf( "$c{bold}%2d$c{reset}. $c{bblue}%s$c{reset}\n" . " $c{bold}Views:$c{reset} %-16s $c{bold}Rating:$c{reset} %-12s $c{bold}Category:$c{reset} %s\n" . " $c{bold}Published:$c{reset} %-12s $c{bold}Duration:$c{reset} %-10s $c{bold}Author:$c{reset} %s\n\n", ++$num => $video->{title}, set_thousands($video->{views}) => sprintf('%.2f', $video->{rating}) => $video->{category}, format_date($video->{published}) => $video->{duration} => $video->{author} ); } elsif ($CONFIG{use_colors}) { # Colorful results (when using --colors or -C) printf( "%s%s%2d%s%s - %s%s%s%s (%sby %s%s%s) (%s%s%s%s)%s\n", $c{cblack} => $c{bold} => ++$num => $c{reset}, $c{cblack} => $c{byellow} => $video->{title} => $c{reset}, $c{cblack} => $c{bpurle} => $video->{author} => $c{reset}, $c{cblack} => $c{bblue} => $video->{duration} => $c{reset}, $c{cblack} => $c{reset} ); } else { # Normal results printf("%s%2d%s - %s (by %s) (%s)\n", $c{bold}, ++$num, $c{reset}, $video->{title}, $video->{author}, $video->{duration}); } } if ($opt{playback}) { @picks = 1 .. @results; foreach_pick(); } { given ($term->readline($prompt{select_video_to_play})) { when (['help', '?']) { print $stdin_help; redo; } when (\&check_user_input) { redo; } when ([qr/^\s*$/, 'next']) { next_page(); } when ('back') { if ( do { $youtube_gdata_url =~ /[&?]start-index=(\d+)/; $1 > $CONFIG{results}; } ) { previous_page(); } else { continue; } } when (/^((?:dis)?like)\s+(\d+)\s*$/) { my ($rating, $i) = ($1, $2); continue if $i == 0 or $i > @results; send_rating_to_video($results[$i - 1]->{code}, $rating); redo; } when (/^c(?:omments)?\s+(\d+)\s*$/) { my $i = $1; continue if $i == 0 or $i > @results; show_comments($results[$i - 1]->{code}); } when (/^fav(?:orite)?\s+(\d+)\s*$/) { my $i = $1; continue if $i == 0 or $i > @results; favorite_video($results[$i - 1]->{code}); redo; } when (/^r(?:elated(?:[- _]videos)?)?\s+(\d+)\s*$/) { my $i = $1; continue if $i == 0 or $i > @results; show_related_videos($results[$i - 1]->{code}); } when (/^sub(?:scribe)?\s+(\d+)\s*$/) { my $i = $1; continue if $i == 0 or $i > @results; subscribe_channel($results[$i - 1]->{author}); } when (/^i(?:nfo)?\s+(\d+)\s*$/) { my $i = $1; continue if $i == 0 or $i > @results; $opt{show_info_only} = 1; get_youtube($results[$i - 1]); $opt{show_info_only} = 0; redo; } when (/^v(?:ideos)?\s+(\d+)\s*$/) { my $i = $1; continue if $i == 0 or $i > @results; videos_from_username($results[$i - 1]->{author}); } when (/^p(?:laylists)?\s+(\d+)\s*$/) { my $i = $1; continue if $i == 0 or $i > @results; playlists_from_username($results[$i - 1]->{author}); } when ('all') { @picks = 1 .. scalar @results; foreach_pick(); } when (chr ord eq q{/} and /$match_regexp/o) { my $match = qr/$1/i; @picks = grep { $results[$_ - 1]->{title} =~ /$match/ } 1 .. @results; if (@picks) { foreach_pick(); } else { warn "\n$c{bold}(X_X) No video matched by the regexp: $c{bgreen}/$match/$c{reset}\n\n"; sleep 1; print_results(); } } when (/\d/ and not /(?:\s|^)[^\d-]/) { # remove numeric arguments (e.g.: -4, -arg=\d); s/(?:\D|^)[-=]+\d+(?:\w+)?//g; # '2..5' or '2-5' to '2 3 4 5', or '3..1 to '3 2 1' s/(\d+)(?:-|\.\.)(\d+)/join q{ }, $1 < $2 ? $1 .. $2 : reverse($2 .. $1);/eg; @picks = grep { /^\d+$/ and $_ > 0 and $_ <= @results } split /[\s,]+/, $_; @picks ? foreach_pick() : continue; } default { search(join(q{ }, get_keywords_from_string($_)) or redo); } } } } sub foreach_pick { while (@picks) { get_youtube($results[shift(@picks) - 1]); } } #---------------------- NEXT PAGE ----------------------# sub next_page { if ($youtube_gdata_url =~ s/[&?]start-index=\K(\d+)/$1 + $CONFIG{results}/e) { if ($opt{playlists}) { search_playlists($youtube_gdata_url, @_); } else { parse_content($youtube_gdata_url); print_results(); } } } #---------------------- PREVIOUS PAGE ----------------------# sub previous_page { if ($youtube_gdata_url =~ s/[&?]start-index=\K(\d+)/$1 - $CONFIG{results}/e) { if ($opt{playlists}) { search_playlists($youtube_gdata_url, @_); } else { parse_content($youtube_gdata_url); print_results(); } } } sub lower_quality { foreach my $itag (sort { $b <=> $a } values %itags) { if (exists($streaming{$itag})) { return $streaming{$itag}; } } } sub select_resolution { given ($CONFIG{resolution}) { when (1080) { return $streaming{1080} // lower_quality(); } when (720) { return $streaming{720} // lower_quality(); } when (480) { return $streaming{480} // lower_quality(); } when (360) { return $streaming{360} // lower_quality(); } when (240) { return $streaming{240} // lower_quality(); #/ } default { return lower_quality(); } } } sub format_itags { my @itags; foreach my $itag (@_) { if (exists($itags{$itag})) { push @itags, $itags{$itag}; } } @itags; } # Getting YouTube closed captions with gcap sub get_closed_caption { my ($code) = @_; unless (exists $constant{cwd}) { $constant{cwd} = rel2abs(curdir()); } # Change dir to $TMP and get the SRT file chdir $CONFIG{tmp_dir}; my $srt_file; my $i = 0; { $srt_file = -e "${code}_$CONFIG{srt_language}.srt" ? "${code}_$CONFIG{srt_language}.srt" : do { opendir(my $dir_h, $CONFIG{tmp_dir}) or return q{}; my $srt = (grep /^\Q$code\E[\w-]*[.](?i:srt)\z/, readdir($dir_h))[0]; closedir $dir_h; $srt; }; unless (defined $srt_file) { system $CONFIG{perl_bin}, $CONFIG{gcap}, $code; if ($? == 0 and not $i++) { redo; } } } return defined $srt_file ? "$CONFIG{mplayer_srt_settings} -sub $srt_file" : q{}; } sub format_date { return "$3.$2.$1" if $_[0] =~ /^(\d{4})-(\d{2})-(\d{2})/; } # Thousand separator sub set_thousands { return 0 unless $_[0]; length($_[0]) > 3 or return $_[0]; my $n = shift; my $l = length($n) - 3; my $i = ($l - 1) % 3 + 1; my $x = substr($n, 0, $i) . $CONFIG{thousand_separator}; while ($i < $l) { $x .= substr($n, $i, 3) . $CONFIG{thousand_separator}; $i += 3; } $x . substr($n, $i); } sub get_youtube { my $info = shift(); if (ref $info ne 'HASH') { parse_content("$CONFIG{feeds_main_url}/videos/$info?v=2"); $info = $results[0] if @results; if (@results and ref $info eq 'HASH' and not defined $info->{code}) { die <<"ERROR"; ** Something is REALLY wrong... Unable to continue!\n Tips: 1. (Edit/delete) the configuration file 2. Run in -debug mode and send '${execname}.debug' to developer ERROR } elsif (ref $info ne 'HASH') { warn "$c{bred}(x_x) Unable to stream:$c{reset} ", sprintf($CONFIG{youtube_video_url}, $info), "\n\n"; return; } } my $code = $info->{code}; my $content = lwp_get("$CONFIG{get_video_info}?&video_id=$code&el=detailpage&ps=default&eurl=&gl=US&hl=en"); my $url = sprintf($CONFIG{youtube_video_url}, $code); $MPLAYER{arguments} = q{}; if ( not $opt{download_video} and -e $CONFIG{gcap} and not exists $MPLAYER{novideo} and $content =~ /&has_cc=True&/) { $MPLAYER{arguments} = get_closed_caption($code); } if ($content =~ /url_encoded_fmt_stream_map=(.+?)&/) { my $streaming = $1; $streaming =~ s/%253A/:/gi; $streaming =~ s{%252F}{/}gi; $streaming =~ s/%2526/&/g; $streaming =~ s/%253D/=/gi; $streaming =~ s/%253F/?/gi; $streaming =~ s/%25252C/,/gi; undef %streaming; my (@streaming_urls) = grep m{^https?:}, split(m{url%3D(.+?)%26}, $streaming); @streaming{format_itags(map m{&itag=(\d+)&}, @streaming_urls)} = grep { exists $itags{ do { m{&itag=(\d+)&}; $1 } } } @streaming_urls; if ($opt{debug}) { while (my ($key, $value) = each %streaming) { print "KEY = $key\nVALUE = $value\n\n"; } } my $rating = sprintf('%.2f', $info->{rating}); if (!defined $info->{description}) { $info->{description} = 'No description available...'; } print "\n$c{bold}=>>$c{reset} Description\n", "$constant{dash_line}\n", "$info->{description}\n$constant{dash_line}\n", "$c{bold}=>>$c{reset} View & Download\n", "$constant{dash_line}\n$c{bold}* URL:$c{reset} "; print STDOUT $url; print "\n$c{bold}* GET:$c{reset} $streaming_urls[0]\n$constant{dash_line}\n", q{ } x ((length($constant{dash_line}) - length($info->{title})) / 2 - 4), "$c{bold}=>>$c{reset} $c{bgreen}$info->{title}$c{reset} $c{bold}<<=$c{reset}\n\n", "** $c{bold}Author$c{reset} : $info->{author}\n", "** $c{bold}Category$c{reset} : $info->{category}\n", "** $c{bold}Duration$c{reset} : $info->{duration}\n", "** $c{bold}Rating$c{reset} : $rating\n", "** $c{bold}Likes$c{reset} : ", set_thousands($info->{likes}) . "\n", "** $c{bold}Dislikes$c{reset} : ", set_thousands($info->{dislikes}) . "\n", "** $c{bold}Favorited$c{reset} : " . set_thousands($info->{favorited}) . "\n", "** $c{bold}Views$c{reset} : " . set_thousands($info->{views}) . "\n", do { $info->{published} ? sprintf("** $c{bold}Published$c{reset} : %s\n", format_date($info->{published})) : q{}; }, "$constant{dash_line}\n"; return if $opt{show_info_only}; # Select one resolution and play video play_or_download(select_resolution(), $info->{title}); } else { # This happens when a video has been deleted or forbidden 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"; if (@results and not $opt{video_id_from_arguments}) { unless (@picks) { sleep 1; print_results(); } } else { unless (@results or $opt{video_id_from_arguments}) { main_quit(); } } } } sub show_related_videos { my ($code) = @_; parse_url("$CONFIG{feeds_main_url}/videos/$code/related"); } sub send_rating_to_video { my ($code, $rating) = @_; my $uri = "$CONFIG{feeds_main_url}/videos/$code/ratings"; say _save( 'POST', $uri, <<"XML_HEADER" XML_HEADER ) ? "\u${rating}d!" : 'Error!'; } sub send_comment_to_video { my ($code, $comment) = @_; return unless length $comment; my $uri = "$CONFIG{feeds_main_url}/videos/$code/comments"; say _save( 'POST', $uri, <<"XML_HEADER" $comment XML_HEADER ) ? 'Comment sent' : 'Error!'; } sub subscribe_channel { my ($user) = @_; my $uri = "$CONFIG{feeds_main_url}/users/default/subscriptions"; say _save( 'POST', $uri, <<"XML_HEADER" $user XML_HEADER ) ? "Successfully subscribed to user: $user" : 'Error!'; } sub favorite_video { my ($code) = @_; my $uri = "$CONFIG{feeds_main_url}/users/default/favorites"; say _save( 'POST', $uri, <<"XML_HEADER" $code XML_HEADER ) ? "** Successfully favorited video: $code" : 'Error!'; } sub _request { my ($req) = @_; my $res = $lwp->request($req); if ($res->is_success) { return $res->content(); } else { warn "Error: $res->code\n", $res->content; return; } } sub _prepare_request { my ($req, $length) = @_; $req->header('GData-Version' => 2); $req->header('Content-Length' => $length) if ($length); if ($lwp_header{Authorization}) { $req->header(Authorization => $lwp_header{Authorization}); $req->header('X-GData-Key' => $lwp_header{'X-GData-Key'}); } else { warn $prompt{needs_login}; } } sub _save { my ($method, $uri, $content) = @_; my $req = HTTP::Request->new("$method" => $uri); $req->content_type('application/atom+xml; charset=UTF-8'); _prepare_request($req, length($content)); $req->content($content); return _request($req); } sub show_comments { my ($code, $index) = @_; print "\n"; $index ||= 1; my $hash = xml2hash( lwp_get( "$CONFIG{feeds_main_url}/videos/$code/comments?" . array_to_gdata_arguments( 'v' => $constant{gdata_version}, 'start-index' => $index ) ) ); my $number = 0; while ( my $gdata = ref $hash->{feed}{entry} eq 'ARRAY' ? $hash->{feed}{entry}[$number] : ref $hash->{feed}{entry} eq 'HASH' ? $hash->{feed}{entry} : $hash->{entry} ) { last unless defined $gdata; printf("$c{bold}%s$c{reset} on %s said:\n\t%s\n\n", $gdata->{author}{name}, format_date($gdata->{updated}), $gdata->{content}); ++$number; last unless ref $hash->{feed}{entry} eq 'ARRAY'; } if ($number == 0) { print "No comments!\n\n"; print_results(); } { given ($term->readline($prompt{comments_next_page})) { when (q{}) { show_comments($code, $index + 25); } when (['?', 'help', 'h']) { print "\n", <<"HELP"; : next page of comments c, comment : sent a comment to this video d, done : return to video results ?, h, help : this message HELP redo; } when (['c', 'comment']) { print "\n$c{bold}=>> $c{bgreen}Write your comment here - press CTRL+D when you are done$c{reset}\n"; chomp(my $comment = join(q{}, )); $comment =~ s/[^\s[:^cntrl:]]+//g; send_comment_to_video($code, $comment); redo; } default { print "\n"; print_results(); } } } return 1; } sub main_quit { write_config_to_file() if $opt{update_config}; exit 0; } main_quit(); package Config; sub _dump { require Data::Dumper; return Data::Dumper::Dumper(shift); } sub _sort_items { my ($data) = @_; my ($items) = $data =~ /\{(.+?)\s*\};?\s*\z/s; $items .= ','; $data = "#!/usr/bin/perl\n\nscalar {" . join( "\n", ( sort { lc $a cmp lc $b } split(/\n/, $items, 0) ) ) . "\n};\n"; $data =~ s{=>\s*'(\d+)',\s*$} {=> $1,}gm; $data =~ s{(.+?)\s*=>\s*(.+)}{ sprintf '%s%*s', $1, 45 - length($1) + length($2), ' => ' . $2; }egm; return $data; } sub save_hash { my ($file, $config) = @_; return unless ref $config eq 'HASH'; open(my $fh, '>', $file) or return; print {$fh} _sort_items(_dump($config)); close $fh; return 1; } 1;