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