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