Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #!/usr/bin/perl
- # youtube-viewer Shell Script
- =head1 NAME
- youtube-viewer - YouTube from command line.
- See: youtube-viewer --help
- youtube-viewer --tricks
- youtube-viewer --examples
- youtube-viewer --stdin-help
- =head1 LICENSE AND COPYRIGHT
- Copyright 2010-2018 Trizen.
- This program is free software; you can redistribute it and/or modify it
- under the terms of either: the GNU General Public License as published
- by the Free Software Foundation; or the Artistic License.
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
- See L<https://dev.perl.org/licenses/> for more information.
- =cut
- use utf8;
- use 5.016;
- use warnings;
- no warnings 'once';
- my $DEVEL; # true in devel mode
- use if ($DEVEL = 0), lib => qw(../lib); # devel mode
- use WWW::YoutubeViewer v3.5.0;
- use WWW::YoutubeViewer::RegularExpressions;
- use File::Spec::Functions qw(
- catdir
- catfile
- curdir
- path
- rel2abs
- tmpdir
- file_name_is_absolute
- );
- binmode(STDOUT, ':utf8');
- my $appname = 'Youtube Viewer';
- my $version = $WWW::YoutubeViewer::VERSION;
- my $execname = 'youtube-viewer';
- # A better <STDIN> support:
- require Term::ReadLine;
- my $term = Term::ReadLine->new("$appname $version");
- # Developer key
- my $key = 'aXalQYmzI8gPkMSLyMhpApfMAiU2b23Qz2nE3mq';
- sub VIDEO_PART () { 'contentDetails,statistics,snippet' }
- # Options (key=>value) goes here
- my %opt;
- my $term_width = 80;
- # Keep track of watched videos by their ID
- my %watched_videos;
- # Unchangeable data goes here
- my %constant = (win32 => ($^O eq 'MSWin32' ? 1 : 0)); # doh
- my $home_dir;
- my $xdg_config_home = $ENV{XDG_CONFIG_HOME};
- if ($xdg_config_home and -d -w $xdg_config_home) {
- require File::Basename;
- $home_dir = File::Basename::dirname($xdg_config_home);
- if (not -d -w $home_dir) {
- $home_dir = curdir();
- }
- }
- else {
- $home_dir =
- $ENV{HOME}
- || $ENV{LOGDIR}
- || ($constant{win32} ? '\Local Settings\Application Data' : ((getpwuid($<))[7] || `echo -n ~`));
- if (not -d -w $home_dir) {
- $home_dir = curdir();
- }
- $xdg_config_home = catdir($home_dir, '.config');
- }
- # Configuration dir/file
- my $config_dir = catdir($xdg_config_home, $execname);
- my $config_file = catfile($config_dir, "$execname.conf");
- my $authentication_file = catfile($config_dir, 'reg.dat');
- my $history_file = catfile($config_dir, 'history.txt');
- if (not -d $config_dir) {
- require File::Path;
- File::Path::make_path($config_dir)
- or warn "[!] Can't create dir '$config_dir': $!";
- }
- sub which_command {
- my ($cmd) = @_;
- if (file_name_is_absolute($cmd)) {
- return $cmd;
- }
- state $paths = [path()];
- foreach my $path (@{$paths}) {
- if (-e (my $cmd_path = catfile($path, $cmd))) {
- return $cmd_path;
- }
- }
- return;
- }
- # Main configuration
- my %CONFIG = (
- video_players => {
- vlc => {
- cmd => q{vlc},
- srt => q{--sub-file *SUB*},
- audio => q{--input-slave *AUDIO*},
- fs => q{--fullscreen},
- arg => q{--quiet --play-and-exit --no-video-title-show --input-title-format *TITLE*},
- novideo => q{--intf dummy --novideo},
- },
- mpv => {
- cmd => q{mpv},
- srt => q{--sub-file *SUB*},
- audio => q{--audio-file *AUDIO*},
- fs => q{--fullscreen},
- arg => q{--really-quiet --title *TITLE* --no-ytdl},
- novideo => q{--no-video},
- },
- mplayer => {
- cmd => q{mplayer},
- srt => q{-sub *SUB*},
- audio => q{-audiofile *AUDIO*},
- fs => q{-fs},
- arg => q{-prefer-ipv4 -really-quiet -title *TITLE*},
- novideo => q{-novideo},
- },
- },
- video_player_selected => (
- $constant{win32}
- ? 'mplayer'
- : undef # auto-defined later
- ),
- combine_multiple_videos => 0,
- # YouTube options
- dash_support => 1,
- dash_mp4_audio => 1,
- maxResults => 20,
- resolution => 'original',
- videoDefinition => undef,
- videoDimension => undef,
- videoLicense => undef,
- safeSearch => undef,
- videoCaption => undef,
- videoDuration => undef,
- videoSyndicated => undef,
- publishedBefore => undef,
- publishedAfter => undef,
- order => undef,
- subscriptions_order => 'relevance',
- hl => 'en_US',
- regionCode => undef,
- # URI options
- youtube_video_url => 'https://www.youtube.com/watch?v=%s',
- # Subtitle options
- srt_languages => ['en', 'es'],
- captions_dir => tmpdir(),
- get_captions => 1,
- auto_captions => 0,
- copy_caption => 0,
- cache_dir => undef, # will be defined later
- # Others
- http_proxy => undef,
- env_proxy => 1,
- confirm => 0,
- debug => 0,
- page => 1,
- colors => $constant{win32} ^ 1,
- clobber => 0,
- skip_if_exists => 1,
- prefer_mp4 => 0,
- fat32safe => $constant{win32},
- fullscreen => 0,
- results_with_details => 0,
- results_with_colors => 0,
- results_fixed_width => 0,
- interactive => 1,
- get_term_width => $constant{win32} ^ 1,
- download_with_wget => undef, # auto-defined later
- download_in_parallel => 0,
- thousand_separator => q{,},
- downloads_dir => curdir(),
- keep_original_video => 0,
- download_and_play => 0,
- autohide_watched => 0,
- highlight_watched => 1,
- highlight_color => 'bold',
- remove_played_file => 0,
- history => 0,
- history_limit => 10_000,
- history_file => $history_file,
- convert_cmd => 'ffmpeg -i *IN* *OUT*',
- convert_to => undef,
- custom_layout => 0,
- custom_layout_format => '*NO*. *TITLE* (*AUTHOR*) (*RATING*) [*TIME*]\n',
- ffmpeg_cmd => 'ffmpeg',
- wget_cmd => 'wget',
- merge_into_mkv => undef, # auto-defined later
- merge_into_mkv_args => '-loglevel warning -c:s srt -c:v copy -c:a copy -disposition:s forced',
- merge_with_captions => 1,
- video_filename_format => '*FTITLE* - *ID*.*FORMAT*',
- );
- local $SIG{__WARN__} = sub { warn @_; ++$opt{_error} };
- my %MPLAYER; # will store video player arguments
- my $base_options = <<'BASE';
- # Base
- [keywords] : search for YouTube videos
- [youtube-url] : play a video by YouTube URL
- :v(ideoid)=ID : play videos by YouTube video IDs
- [playlist-url] : display videos from a playlistURL
- :playlist=ID : display videos from a playlistID
- BASE
- my $action_options = <<'ACTIONS';
- # Actions
- :login : will prompt you for login
- :logout : will delete the authentication key
- ACTIONS
- my $control_options = <<'CONTROL';
- # Control
- :n(ext) : get the next page of results
- :b(ack) : get the previous page of results
- CONTROL
- my $other_options = <<'OTHER';
- # Others
- :r(eturn) : return to previous section
- :refresh : refresh the current list of results
- :dv=i : display the data structure of result i
- -argv -argv2=v : apply some arguments (e.g.: -u=google)
- :reset, :reload : restart the application
- :q, :quit, :exit : close the application
- OTHER
- my $notes_options = <<'NOTES';
- NOTES:
- 1. You can specify more options in a row, separated by spaces.
- 2. A stdin option is valid only if it begins with '=', ';' or ':'.
- 3. Quoting a group of space separated keywords or option-values,
- the group will be considered a single keyword or a single value.
- NOTES
- my $general_help = <<"HELP";
- $action_options
- $control_options
- $other_options
- $notes_options
- Examples:
- 3 : select the 3rd result
- -V funny cats : search for videos
- -p classical music : search for playlists of videos
- HELP
- my $playlists_help = <<"PL_HELP" . $general_help;
- # Playlists
- :pp=i,i : play videos from the selected playlists
- PL_HELP
- my $comments_help = <<"COM_HELP" . $general_help;
- # Comments
- :c(omment) : send a comment to this video
- COM_HELP
- my $complete_help = <<"STDIN_HELP";
- $base_options
- $control_options
- $action_options
- # YouTube
- :i(nfo)=i,i : display more information
- :d(ownload)=i,i : download the selected videos
- :c(omments)=i : display video comments
- :r(elated)=i : display related videos
- :a(uthor)=i : display author's latest uploads
- :p(laylists)=i : display author's playlists
- :ps=i, :s2p=i,i : save videos to a post-selected playlist
- :subscribe=i : subscribe to author's channel
- :(dis)like=i : like or dislike a video
- :fav(orite)=i : favorite a video
- # Playing
- <number> : play the corresponding video
- 3-8, 3..8 : same as 3 4 5 6 7 8
- 8-3, 8..3 : same as 8 7 6 5 4 3
- 8 2 12 4 6 5 1 : play the videos in a specific order
- 10.. : play all the videos onwards from 10
- :q(ueue)=i,i,... : enqueue videos for playing them later
- :pq, :play-queue : play the enqueued videos (if any)
- :anp, :nnp : auto-next-page, no-next-page
- :play=i,i,... : play a group of selected videos
- :regex=my?[regex] : play videos matched by a regex (/i)
- :kregex=KEY,RE : play videos if the value of KEY matches the RE
- $other_options
- $notes_options
- ** Examples:
- :regex="\\w \\d" -> play videos matched by a regular expression.
- :info=1 -> show extra information for the first video.
- :d18-20,1,2 -> download the selected videos: 18, 19, 20, 1 and 2.
- 3 4 :next 9 -> play the 3rd and 4th videos from the current
- page, go to the next page and play the 9th video.
- STDIN_HELP
- {
- my $config_documentation = <<"EOD";
- #!/usr/bin/perl
- # $appname $version - configuration file
- EOD
- sub dump_configuration {
- require Data::Dump;
- open my $config_fh, '>', $config_file
- or do { warn "[!] Can't open '${config_file}' for write: $!"; return };
- my $dumped_config = q{our $CONFIG = } . Data::Dump::pp(\%CONFIG) . "\n";
- print $config_fh $config_documentation, $dumped_config;
- close $config_fh;
- }
- }
- if (not -e $config_file or -z _ or $opt{reconfigure}) {
- dump_configuration();
- }
- our $CONFIG;
- require $config_file; # Load the configuration file
- if (ref $CONFIG ne 'HASH') {
- die "[ERROR] Invalid configuration file!\n\t\$CONFIG is not an HASH ref!";
- }
- # Add audio support to players (backwards compatibility)
- while (my ($player, $data) = each %{$CONFIG->{video_players}}) {
- if ( exists $CONFIG{video_players}{$player}
- and not exists $data->{audio}
- and exists $CONFIG{video_players}{$player}{audio}) {
- $data->{audio} = $CONFIG{video_players}{$player}{audio};
- }
- }
- # Get valid config keys
- my @valid_keys = grep { exists $CONFIG{$_} } keys %{$CONFIG};
- @CONFIG{@valid_keys} = @{$CONFIG}{@valid_keys};
- {
- my $update_config = 0;
- # Define the cache directory
- if (not defined $CONFIG{cache_dir}) {
- my $cache_dir =
- ($ENV{XDG_CACHE_HOME} and -d -w $ENV{XDG_CACHE_HOME})
- ? $ENV{XDG_CACHE_HOME}
- : catdir($home_dir, '.cache');
- if (not -d -w $cache_dir) {
- $cache_dir = catdir(curdir(), '.cache');
- }
- $CONFIG{cache_dir} = catdir($cache_dir, 'youtube-viewer');
- $update_config = 1;
- }
- # Locating a video player
- if (not defined $CONFIG{video_player_selected}) {
- foreach my $key (sort keys %{$CONFIG{video_players}}) {
- if (defined(my $abs_player_path = which_command($CONFIG{video_players}{$key}{cmd}))) {
- $CONFIG{video_players}{$key}{cmd} = $abs_player_path;
- $CONFIG{video_player_selected} = $key;
- $update_config = 1;
- last;
- }
- }
- }
- # Download with wget if it is installed
- if (not defined $CONFIG{download_with_wget}) {
- if (-x '/usr/bin/wget') {
- $CONFIG{wget_cmd} = '/usr/bin/wget';
- $CONFIG{download_with_wget} = 1;
- }
- elsif (-x '/usr/local/bin/wget') {
- $CONFIG{wget_cmd} = '/usr/local/bin/wget';
- $CONFIG{download_with_wget} = 1;
- }
- else {
- $CONFIG{download_with_wget} = 0;
- }
- $update_config = 1;
- }
- # Merge into MKV if ffmpeg is installed
- if (not defined $CONFIG{merge_into_mkv}) {
- if (-x '/usr/bin/ffmpeg') {
- $CONFIG{ffmpeg_cmd} = '/usr/bin/ffmpeg';
- $CONFIG{merge_into_mkv} = 1;
- }
- elsif (-x '/usr/local/bin/ffmpeg') {
- $CONFIG{ffmpeg_cmd} = '/usr/local/bin/ffmpeg';
- $CONFIG{merge_into_mkv} = 1;
- }
- else {
- $CONFIG{merge_into_mkv} = 0;
- }
- $update_config = 1;
- }
- foreach my $key (keys %CONFIG) {
- if (not exists $CONFIG->{$key}) {
- $update_config = 1;
- last;
- }
- }
- dump_configuration() if $update_config;
- }
- # Create the cache directory (if needed)
- if (not -d $CONFIG{cache_dir}) {
- require File::Path;
- File::Path::make_path($CONFIG{cache_dir})
- or warn "[!] Can't create dir `$CONFIG{cache_dir}': $!";
- }
- @opt{keys %CONFIG} = values(%CONFIG);
- if ($opt{history}) {
- # Create the history file.
- if (not -e $opt{history_file}) {
- open my $fh, '>', $opt{history_file}
- or warn "[!] Can't create the history file `$opt{history_file}': $!";
- }
- # Add history to Term::ReadLine
- $term->ReadHistory($opt{history_file});
- # All history entries
- my @entries = $term->history_list;
- # Rewrite the history file, when the history_limit has been reached.
- if ($opt{history_limit} > 0 and @entries > $opt{history_limit}) {
- # Try to create a backup, first
- require File::Copy;
- File::Copy::cp($opt{history_file}, "$opt{history_file}.bak");
- if (open my $fh, '>', $opt{history_file}) {
- say {$fh} join("\n", @entries[(@entries - $opt{history_limit} + rand($opt{history_limit} >> 1)) .. $#entries]);
- close $fh;
- }
- }
- }
- {
- my $i = length $key;
- $key =~ s/(.{$i})(.)/$2$1/g while --$i;
- }
- my $yv_obj = WWW::YoutubeViewer->new(
- escape_utf8 => 1,
- key => $key,
- config_dir => $config_dir,
- cache_dir => $opt{cache_dir},
- lwp_env_proxy => $opt{env_proxy},
- authentication_file => $authentication_file,
- );
- {
- $yv_obj->set_client_id('923751928481.apps.googleusercontent.com');
- $yv_obj->set_client_secret("\26/Ae]3\b\6\x186a:*#0\32\t\f\n\27\17GC`" ^ substr($key, -24));
- $yv_obj->set_redirect_uri('urn:ietf:wg:oauth:2.0:oob');
- }
- require WWW::YoutubeViewer::Utils;
- my $yv_utils = WWW::YoutubeViewer::Utils->new(youtube_url_format => $opt{youtube_video_url},
- thousand_separator => $opt{thousand_separator},);
- { # Apply the configuration file
- my %temp = %CONFIG;
- apply_configuration(\%temp);
- }
- #---------------------- YOUTUBE-VIEWER USAGE ----------------------#
- sub help {
- my $eqs = q{=} x 30;
- local $" = ', ';
- print <<"HELP";
- \n $eqs \U$appname\E $eqs
- usage: $execname [options] ([url] | [keywords])
- == Base ==
- [URL] : play an YouTube video by URL
- [keywords] : search for YouTube videos
- [playlist URL] : display a playlist of YouTube videos
- == YouTube Options ==
- * Categories
- -c --categories : display the available YouTube categories
- -hl --catlang=s : language for categories (default: en_US)
- * Region
- --region=s : set the region code (default: US)
- * Videos
- -uv --user-vid=s : list videos uploaded by a specific user
- -cv --channel-vid=s : list videos uploaded to a specific channel
- -uf --user-fav=s : list the videos favorited by a specific user
- -id --videoids=s,s : play YouTube videos by their IDs
- -rv --related=s : show related videos for a video ID or URL
- --search=s : search for YouTube videos (default mode)
- * Playlists
- -p --playlists : search for playlists of videos
- --pid=s : list a playlist of videos by playlistID
- --pp=s,s : play the videos from the given playlist IDs
- --ps=s : add videos by ID or URL to a post-selected playlist
- or in a given playlistID specified with `--pid`
- --position=i : the position in a playlist where to add a video
- -up --user-pl=s : list the playlists created by a specific user
- -cp --channel-pl=s : list the playlists belonging to a specific channel ID
- * Channels
- --channels : search for Youtube channels
- * Comments
- --comments=s : display comments for a YouTube video by ID or URL
- * Filtering
- --author=s : search in videos uploaded by a specific user
- --channel-id=s : search in videos belonging to a specific channel ID
- --duration=s : filter search results based on video length
- valid values are: short, medium, long
- --caption=s : only videos with/without closed captions
- valid values are: any, closedCaption, none
- --category=i : search only for videos in a specific category ID
- --safe-search=s : YouTube will skip restricted videos for your location
- valid values are: none, moderate, strict
- --order=s : order the results using a specific sorting method
- valid values: date rating viewCount title videoCount
- --within=s : show only videos uploaded within the specified time
- valid values are: Nd, Nm, Ny, where N is a number
- --hd! : search only for videos available in at least 720p
- --vd=s : set the video definition (any, high or standard)
- --page=i : get results starting with a specific page
- --results=i : how many results to display per page (max: 50)
- -2 -3 -4 -7 -1 : resolutions: 240p, 360p, 480p, 720p and 1080p
- --resolution=s : supported resolutions: original, 2160p, 1440p,
- 1080p, 720p, 480p, 360p, 240p, 144p, audio.
- * Account
- --login : will prompt for authentication (OAuth 2.0)
- --logout : will delete the authentication key
- * [GET] Personal
- -F --favorites:s : show the latest favorited videos *
- -S --subscriptions:s : show the subscribed channels *
- -SV --subs-videos:s : show the subscription videos *
- --subs-order=s : change the subscription order
- valid values: alphabetical, relevance, unread
- -L --likes : show the videos that you liked on YouTube *
- --dislikes : show the videos that you disliked on YouTube *
- * [POST] Personal
- --subscribe=s : subscribe to a channel *
- --user-subscribe=s : subscribe to a channel via username *
- --favorite=s : favorite a YouTube video by URL or ID *
- --like=s : send a 'like' rating to a video URL or ID *
- --dislike=s : send a 'dislike' rating to a video URL or ID *
- == Player Options ==
- * Arguments
- -f --fullscreen! : set the fullscreen mode for the selected video player
- -n --novideo! : play the music only without a video in the foreground
- --vo=s : specify the video output for MPlayer
- --af=s : specify an audio filter for MPlayer
- --append-arg=s : append some command-line parameters to the media player
- --player=s : select a player to stream videos
- available players: @{[keys %{$CONFIG->{video_players}}]}
- == Download Options ==
- * Download
- -d --download! : activate the download mode
- -dp --dl-play! : play the video after download (with -d)
- -rp --rem-played! : delete a local video after played (with -dp)
- --wget-dl! : download videos with wget (recommended)
- --dl-parallel : download multiple videos at once
- --clobber! : overwrite an existent video (with -d)
- --skip-if-exists! : don't download videos which already exists (with -d)
- --copy-caption! : copy and rename the caption for downloaded videos
- --downloads-dir=s : downloads directory (set: '$opt{downloads_dir}')
- --filename=s : set a custom format for the video filename (see: -T)
- --fat32safe! : makes filenames FAT32 safe (includes Unicode)
- --mkv-merge! : merge audio and video into an MKV container
- --merge-captions! : include closed-captions into the MKV container
- * Convert
- --convert-cmd=s : command for converting videos after download
- which include the *IN* and *OUT* tokens
- --convert-to=s : convert video to a specific format (with -d)
- --keep-original! : keep the original video after converting
- == Other Options ==
- * Behavior
- -A --all! : play all the video results in order
- -B --backwards! : play all video results in reverse order
- -s --shuffle! : shuffle the results of videos and playlists
- -I --interactive! : interactive mode, prompting for user input
- --std-input=s : use this value as the first standard input
- --max-seconds=i : ignore videos longer than i seconds
- --min-seconds=i : ignore videos shorter than i seconds
- --combine-multi! : combine multiple videos into one play instance
- --get-term-width! : allow $execname to read your terminal width
- --autohide! : automatically hide watched videos
- --highlight! : remember and highlight selected videos
- --confirm! : show a confirmation message after each play
- --prefer-mp4! : prefer videos in MP4 format, instead of WEBM
- * Closed-captions
- --get-captions! : download the closed captions for videos
- --auto-captions! : include or exclude auto-generated captions
- --captions-dir=s : the directory where to download the .srt files
- * Config
- -U --update-config! : update the configuration file before exit
- * Output
- -C --colorful! : use colors to delimit the video results
- -D --details! : a new look for the results, with more details
- -W --fixed-width! : adjust the results to fit inside the term width
- --custom-layout! : display the results using a custom layout (see conf)
- -i --info=s : show some info for a videoID or URL
- -e --extract=s : extract information from videos (see: -T)
- --extract-file=s : extract the information from videos in this file
- --dump=format : dump metadata information in `videoID.format` files
- valid formats: json, perl
- -q --quiet : do not display any warning
- --really-quiet : do not display any warning or output
- --escape-info! : quotemeta() the fields of the `--extract`
- --use-colors! : enable or disable the ANSI colors for text
- * Other
- --proxy=s : set HTTP(S)/SOCKS proxy: 'proto://domain.tld:port/'
- If authentication required,
- use 'proto://user:pass\@domain.tld:port/'
- --dash! : include or exclude the DASH itags
- --dash-mp4a! : include or exclude the itags for MP4 audio streams
- Help options:
- -T --tricks : show more 'hidden' features of $execname
- -E --examples : show some useful usage examples for $execname
- -H --stdin-help : show the valid stdin options for $execname
- -v --version : print version and exit
- -h --help : print help and exit
- --debug:[1,2] : see behind the scenes
- NOTES:
- * -> requires authentication
- ! -> the argument can be negated with '--no'
- =i -> requires an integer argument
- =s -> requires an argument
- :s -> can take an optional argument
- =s,s -> can take more arguments separated by commas
- HELP
- main_quit(0);
- }
- sub wrap_text {
- my (%args) = @_;
- require Text::Wrap;
- local $Text::Wrap::columns = ($args{columns} || $term_width) - 8;
- my $text = "@{$args{text}}";
- $text =~ tr{\r}{}d;
- return eval { Text::Wrap::wrap($args{i_tab}, $args{s_tab}, $text) } // $text;
- }
- sub tricks {
- print <<"TRICKS";
- == youtube-viewer -- tips and tricks ==
- -> Playing videos
- > To stream the videos in other players, you need to change the
- configuration file. Where it says "video_player_selected", change it
- to any player which is defined inside the "video_players" hash.
- -> Arguments
- > Almost all boolean arguments can be negated with a "--no-" prefix.
- > Arguments that require an ID/URL, you can specify more than one,
- separated by whitespace (quoted), or separated by commas.
- -> My channel
- > Starting with version 3.2.1, it's possible to use the string "mine"
- in place where a channel ID is required. Doing this, "mine" will be
- replaced with your channel ID. (requires authentication)
- Examples:
- $execname --channel-playlists=mine
- $execname --channel-videos=mine
- $execname --likes=mine
- $execname --favorites=mine
- -> More STDIN help:
- > ":r", ":return" will return to the previous section.
- For example, if you search for playlists, then select a playlist
- of videos, inserting ":r" will return back to the playlist results.
- Also, for the previous page, you can insert ':b', but ':r' is faster!
- > "6" (quoted) or -V=6 will search for videos with the keyword '6'.
- > If a stdin option is followed by one or more digits, the equal sign,
- which separates the option from value, can be omitted.
- For example:
- :i2,4 is equivalent with :i=2,4
- :d1-5 is equivalent with :d=1,2,3,4,5
- :c10 is equivalent with :c=10
- > When more videos are selected to play, you can stop them by
- pressing CTRL+C. $execname will return to the previous section.
- > Space inside the values of STDIN options, can be either quoted
- or backslashed.
- For example:
- :re=video\\ title == :re="video title"
- > ":anp" stands for the "Auto Next Page". How do we use it?
- Well, let's search for some videos. Now, if we'd want to play
- only the videos matched by a regex, we'd say :re="REGEX".
- But, what if we'd want to play the videos from the next pages too?
- In this case, ":anp" is your friend. Use it wisely!
- -> Special tokens:
- *ID* : the YouTube video ID
- *AUTHOR* : the author name of the video
- *CHANNELID* : the channel ID of the video
- *RESOLUTION* : the resolution of the video
- *VIEWS* : the number of views
- *LIKES* : the number of likes
- *DISLIKES* : the number of dislikes
- *RATING* : the rating of the video from 0 to 5
- *COMMENTS* : the number of comments
- *DURATION* : the duration of the video in seconds
- *DIMENSION* : the dimension of the video (2D or 3D)
- *DEFINITION* : the definition of the video (HD or SD)
- *TIME* : the duration of the video in HH::MM::SS
- *TITLE* : the title of the video
- *FTITLE* : the title of the video (filename safe)
- *DESCRIPTION* : the description of the video
- *URL* : the YouTube URL of the video
- *ITAG* : the itag value of the video
- *FORMAT* : the extension of the video (without the dot)
- *CAPTION* : true if the video has caption.
- *SUB* : the local subtitle file (if any)
- *AUDIO* : the audio URL of the video (only in DASH mode)
- *VIDEO* : the video URL of the video (it might not contain audio)
- *AOV* : audio URL (if any) or video URL (in this order)
- -> Special escapes:
- \\t tab
- \\n newline
- \\r return
- \\f form feed
- \\b backspace
- \\a alarm (bell)
- \\e escape
- -> Extracting information from videos:
- > Extracting information can be achieved by using the "--extract" command-line
- option which takes a given format as its argument, which is defined by using
- special tokens, special escapes or literals.
- Example:
- $execname --no-interactive --extract '*TITLE* (*ID*)' [URL]
- -> Configuration file: $config_file
- -> Donations gladly accepted:
- https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=75FUVBE6Q73T8
- TRICKS
- main_quit(0);
- }
- sub examples {
- print <<"EXAMPLES";
- ==== COMMAND LINE EXAMPLES ====
- Command: $execname -A -n -4 russian music -category=10
- Results: play all the video results (-A)
- only audio, no video (-n)
- quality 480p (-4)
- search for "russian music"
- in the "10" category, which is the Music category.
- -A will include the videos from the next pages as well.
- Command: $execname --comments 'https://www.youtube.com/watch?v=U6_8oIPFREY'
- Results: show video comments for a specific video URL or videoID
- Command: $execname --results=5 -up=khanacademy -D
- Results: set 5 results,
- get playlists created by a specific user
- and print them with details (-D)
- Command: $execname --author=UCBerkeley atom
- Results: search only in videos uploaded by a specific author
- Command: $execname -S=vsauce
- Results: get the video subscriptions for a username
- Command: $execname --page=2 -u=Google
- Results: show latest videos uploaded by Google,
- starting with the page number 2.
- Command: $execname cats --order=viewCount --duration=short
- Results: search for 'cats' videos, ordered by ViewCount and short duration.
- Command: $execname --channels russian music
- Results: search for channels.
- Command: $execname -uf=Google
- Results: show latest videos favorited by a user.
- ==== USER INPUT EXAMPLES ====
- A STDIN option can begin with ':', ';' or '='.
- Command: <ENTER>, :n, :next
- Results: get the next page of results.
- Command: :b, :back (:r, :return)
- Results: get the previous page of results.
- Command: :i4..6, :i7-9, :i20-4, :i2, :i=4, :info=4
- Results: show extra information for the selected videos.
- Command: :d5,2, :d=3, :download=8
- Results: download the selected videos.
- Command: :c2, :comments=4
- Results: show comments for a selected video.
- Command: :r4, :related=6
- Results: show related videos for a selected video.
- Command: :a14, :author=12
- Results: show videos uploaded by the author who uploaded the selected video.
- Command: :p9, :playlists=14
- Results: show playlists created by the author who uploaded the selected video.
- Command: :subscribe=7
- Results: subscribe to the author's channel who uploaded the selected video.
- Command: :like=2, :dislike=4,5
- Results: like or dislike the selected videos.
- Command: :fav=4, :favorite=3..5
- Results: favorite the selected videos.
- Command: 3, 5..7, 12-1, 9..4, 2 3 9
- Results: play the selected videos.
- Command: :q3,5, :q=4, :queue=3-9
- Results: enqueue the selected videos to play them later.
- Command: :pq, :play-queue
- Results: play the videos enqueued by the :queue option.
- Command: :re="^Linux"
- Results: play videos matched by a regex.
- Example: valid title: "Linux video"
- Command: :regex="linux.*part \\d+/\\d+"
- Example: valid title: "Introduction to Linux (part 1/4)"
- Command: :anp 1 2 3
- Results: play the first three videos from every page.
- Command: :r, :return
- Results: return to the previous section.
- EXAMPLES
- main_quit(0);
- }
- sub stdin_help {
- print $complete_help;
- main_quit(0);
- }
- # Print version
- sub version {
- print "YouTube Viewer $version\n";
- main_quit(0);
- }
- sub apply_configuration {
- my ($opt, $keywords) = @_;
- if ($yv_obj->get_debug == 2
- or (defined($opt->{debug}) && $opt->{debug} == 2)) {
- require Data::Dump;
- say "=>> Options with keywords: <@{$keywords}>";
- Data::Dump::pp($opt);
- }
- # ... BASIC OPTIONS ... #
- if (delete $opt->{quiet}) {
- close STDERR;
- }
- if (delete $opt->{really_quiet}) {
- close STDERR;
- close STDOUT;
- }
- # ... YOUTUBE OPTIONS ... #
- foreach my $option_name (
- qw(
- videoCaption maxResults order
- videoDefinition videoCategoryId
- videoDimension videoDuration
- videoEmbeddable videoLicense
- videoSyndicated channelId
- publishedAfter publishedBefore
- safeSearch regionCode debug hl
- http_proxy page subscriptions_order
- )
- ) {
- if (defined $opt->{$option_name}) {
- my $code = \&{"WWW::YoutubeViewer::set_$option_name"};
- my $value = delete $opt->{$option_name};
- my $set_value = $yv_obj->$code($value);
- if (not defined($set_value) or $set_value ne $value) {
- warn "\n[!] Invalid value <$value> for option <$option_name>\n";
- }
- }
- }
- if (defined $opt->{prefer_mp4}) {
- $yv_obj->set_prefer_mp4(delete($opt->{prefer_mp4}) ? 1 : 0);
- }
- if (defined $opt->{hd}) {
- $yv_obj->set_videoDefinition(delete($opt->{hd}) ? 'high' : 'any');
- }
- if (defined $opt->{author}) {
- my $username = delete $opt->{author};
- $yv_obj->set_channelId($yv_obj->channel_id_from_username($username) // $username);
- }
- if (defined $opt->{within}) {
- my $value = delete $opt->{within};
- if ($value =~ /^\s*(\d+(?:\.\d+)?)([dmy])/i) {
- my $date = $yv_utils->period_to_date($1, $2);
- $yv_obj->set_publishedAfter($date);
- }
- else {
- warn "\n[!] Invalid value <$value> for option `--within`!\n";
- }
- }
- if (defined $opt->{more_results}) {
- $yv_obj->set_maxResults(delete($opt->{more_results}) ? 50 : $CONFIG{maxResults});
- }
- if (delete $opt->{authenticate}) {
- authenticate();
- }
- if (delete $opt->{logout}) {
- logout();
- }
- # ... OTHER OPTIONS ... #
- if (defined $opt->{extract_info_file}) {
- open my $fh, '>:utf8', delete($opt->{extract_info_file});
- $opt{extract_info_fh} = $fh;
- }
- if (defined $opt->{colors}) {
- $opt{_colors} = $opt->{colors};
- if (delete $opt->{colors}) {
- require Term::ANSIColor;
- *colored = \&Term::ANSIColor::colored;
- *colorstrip = \&Term::ANSIColor::colorstrip;
- }
- else {
- *colored = sub { $_[0] };
- *colorstrip = sub { $_[0] };
- }
- }
- # ... SUBROUTINE CALLS ... #
- if (defined $opt->{subscribe_channel}) {
- subscribe_to_channels(split(/[,\s]+/, delete $opt->{subscribe_channel}));
- }
- if (defined $opt->{subscribe_username}) {
- subscribe_to_usernames(split(/[,\s]+/, delete $opt->{subscribe_username}));
- }
- if (defined $opt->{favorite_video}) {
- favorite_videos(split(/[,\s]+/, delete $opt->{favorite_video}));
- }
- if (defined $opt->{playlist_save}) {
- my @ids = split(/[,\s]+/, delete $opt->{playlist_save});
- if (defined $opt->{playlist_id}) {
- save_to_playlist(get_valid_playlist_id(delete $opt->{playlist_id}) // (return), @ids);
- }
- else {
- select_and_save_to_playlist(@ids);
- }
- }
- if (defined $opt->{like_video}) {
- rate_videos('like', split(/[,\s]+/, delete $opt->{like_video}));
- }
- if (defined $opt->{dislike_video}) {
- rate_videos('dislike', split(/[,\s]+/, delete $opt->{dislike_video}));
- }
- if (defined $opt->{play_video_ids}) {
- get_and_play_video_ids(split(/[,\s]+/, delete $opt->{play_video_ids}));
- }
- if (defined $opt->{play_playlists}) {
- get_and_play_playlists(split(/[,\s]+/, delete $opt->{play_playlists}));
- }
- if (defined $opt->{playlist_id}) {
- my $playlistID = get_valid_playlist_id(delete($opt->{playlist_id})) // return;
- get_and_print_videos_from_playlist($playlistID);
- }
- if (defined $opt->{search_playlists}) {
- my $value = delete($opt->{search_playlists});
- if ($value =~ /$valid_playlist_id_re/ and not @{$keywords}) {
- get_and_print_videos_from_playlist($value);
- }
- else {
- print_playlists($yv_obj->search_playlists([$value, @{$keywords}]));
- }
- }
- if (defined $opt->{search_videos}) {
- my $value = delete $opt->{search_videos};
- print_videos($yv_obj->search_videos([$value, @{$keywords}]));
- }
- if (defined $opt->{search_channels}) {
- my $value = delete $opt->{search_channels};
- print_channels($yv_obj->search_channels([$value, @{$keywords}]));
- }
- if (delete $opt->{categories}) {
- print_categories($yv_obj->video_categories);
- }
- if (defined $opt->{user_videos}) {
- my $id = delete $opt->{user_videos};
- if ($id =~ /$valid_channel_id_re/) {
- print_videos($yv_obj->uploads_from_username($+{channel_id}));
- }
- else {
- warn_invalid("username", $id);
- }
- }
- if (defined $opt->{channel_id_videos}) {
- my $id = delete $opt->{channel_id_videos};
- if ($id =~ /$valid_channel_id_re/) {
- print_videos($yv_obj->uploads($+{channel_id}));
- }
- else {
- warn_invalid("channelID", $id);
- }
- }
- if (defined $opt->{subscriptions}) {
- my $username = delete $opt->{subscriptions};
- print_channels($username ? $yv_obj->subscriptions_from_username($username) : $yv_obj->subscriptions);
- }
- if (defined $opt->{subscription_videos}) {
- my $username = delete $opt->{subscription_videos};
- print_videos($username ? $yv_obj->subscription_videos_from_username($username) : $yv_obj->subscription_videos);
- }
- if (defined $opt->{related_videos}) {
- get_and_print_related_videos(split(/[,\s]+/, delete($opt->{related_videos})));
- }
- if (defined $opt->{user_playlists}) {
- my $id = delete $opt->{user_playlists};
- if ($id =~ /$valid_channel_id_re/) {
- print_playlists($yv_obj->playlists_from_username($+{channel_id}));
- }
- else {
- warn_invalid("username", $id);
- }
- }
- if (defined $opt->{channel_playlists}) {
- my $id = delete $opt->{channel_playlists};
- if ($id =~ /$valid_channel_id_re/) {
- print_playlists($yv_obj->playlists($+{channel_id}));
- }
- else {
- warn_invalid("channelID", $id);
- }
- }
- if (defined $opt->{favorites}) {
- my $channel_id = delete($opt->{favorites});
- print_videos(
- $channel_id
- ? $yv_obj->favorites($channel_id)
- : $yv_obj->favorites
- );
- }
- if (defined $opt->{likes}) {
- my $channel_id = delete($opt->{likes});
- print_videos(
- $channel_id
- ? $yv_obj->likes($channel_id)
- : $yv_obj->my_likes
- );
- }
- if (defined $opt->{dislikes}) {
- delete $opt->{dislikes};
- print_videos($yv_obj->my_dislikes);
- }
- if (defined $opt->{user_favorited_videos}) {
- my $username = delete $opt->{user_favorited_videos};
- if ($username =~ /$valid_channel_id_re/) {
- print_videos($yv_obj->favorites_from_username($+{channel_id}));
- }
- else {
- warn_invalid("username", $username);
- }
- }
- if (defined $opt->{user_liked_videos}) {
- my $username = delete $opt->{user_liked_videos};
- if ($username =~ /$valid_channel_id_re/) {
- print_videos($yv_obj->likes_from_username($+{channel_id}));
- }
- else {
- warn_invalid("username", $username);
- }
- }
- if (defined $opt->{get_comments}) {
- get_and_print_comments(split(/[,\s]+/, delete($opt->{get_comments})));
- }
- if (defined $opt->{print_video_info}) {
- get_and_print_video_info(split(/[,\s]+/, delete $opt->{print_video_info}));
- }
- }
- sub parse_arguments {
- my ($keywords) = @_;
- state $x = do {
- require Getopt::Long;
- Getopt::Long::Configure('no_ignore_case');
- };
- Getopt::Long::GetOptions(
- # Main options
- 'help|usage|h|?' => \&help,
- 'examples|E' => \&examples,
- 'stdin-help|shelp|sh|H' => \&stdin_help,
- 'tricks|tips|T' => \&tricks,
- 'version|v' => \&version,
- 'update-config|U!' => \&dump_configuration,
- # Resolutions
- '240p|2' => sub { $opt{resolution} = 240 },
- '360p|3' => sub { $opt{resolution} = 360 },
- '480p|4' => sub { $opt{resolution} = 480 },
- '720p|7' => sub { $opt{resolution} = 720 },
- '1080p|1' => sub { $opt{resolution} = 1080 },
- 'res|resolution=s' => \$opt{resolution},
- 'comments=s' => \$opt{get_comments},
- 'search|videos|V:s' => \$opt{search_videos},
- 'video-ids|videoids|id|ids=s' => \$opt{play_video_ids},
- 'c|categories' => \$opt{categories},
- 'channels|search-channels:s' => \$opt{search_channels},
- 'subscriptions|S:s' => \$opt{subscriptions},
- 'subs-videos|SV:s' => \$opt{subscription_videos},
- 'subs-order=s' => \$opt{subscriptions_order},
- 'favorites|fv|favorited-videos|F:s' => \$opt{favorites},
- 'likes|L:s' => \$opt{likes},
- 'dislikes' => \$opt{dislikes},
- 'subscribe=s' => \$opt{subscribe_channel},
- 'user-subscribe=s' => \$opt{subscribe_username},
- 'cv|channel|channel-videos=s' => \$opt{channel_id_videos},
- 'cp|channel-playlists=s' => \$opt{channel_playlists},
- # English-UK friendly
- 'favorite|favourite|favorite-video|favourite-video|fav=s' => \$opt{favorite_video},
- 'login|authenticate' => \$opt{authenticate},
- 'logout' => \$opt{logout},
- 'user|user-videos|u|uv=s' => \$opt{user_videos},
- 'user-playlists|up=s' => \$opt{user_playlists},
- 'user-favorites|uf=s' => \$opt{user_favorited_videos},
- 'user-likes|ul=s' => \$opt{user_liked_videos},
- 'related-videos|rl|rv=s' => \$opt{related_videos},
- 'http_proxy|http-proxy|proxy=s' => \$opt{http_proxy},
- 'catlang|cl|hl=s' => \$opt{hl},
- 'category|cat-id|cat=i' => \$opt{videoCategoryId},
- 'r|region|region-code=s' => \$opt{regionCode},
- 'orderby|order|order-by=s' => \$opt{order},
- 'duration=s' => \$opt{videoDuration},
- 'within=s' => \$opt{within},
- 'max-seconds|max_seconds=i' => \$opt{max_seconds},
- 'min-seconds|min_seconds=i' => \$opt{min_seconds},
- 'like=s' => \$opt{like_video},
- 'dislike=s' => \$opt{dislike_video},
- 'author=s' => \$opt{author},
- 'channel-id=s' => \$opt{channelId},
- 'all|A|play-all!' => \$opt{play_all},
- 'backwards|B!' => \$opt{play_backwards},
- 'input|std-input=s' => \$opt{std_input},
- 'use-colors|colors|colored!' => \$opt{colors},
- 'playlists|p|pl|playlist:s' => \$opt{search_playlists},
- 'pid|playlist-id=s' => \$opt{playlist_id},
- 'play-playlists|pp=s' => \$opt{play_playlists},
- 'debug:1' => \$opt{debug},
- 'download|dl|d!' => \$opt{download_video},
- 'safe-search|safeSearch=s' => \$opt{safeSearch},
- 'vd|video-definition=s' => \$opt{videoDefinition},
- 'hd|high-definition!' => \$opt{hd},
- 'I|interactive!' => \$opt{interactive},
- 'convert-to|convert_to=s' => \$opt{convert_to},
- 'keep-original-video!' => \$opt{keep_original_video},
- 'e|extract|extract-info=s' => \$opt{extract_info},
- 'extract-file=s' => \$opt{extract_info_file},
- 'escape-info!' => \$opt{escape_info},
- 'dump=s' => sub {
- my (undef, $format) = @_;
- $opt{dump} = (
- ($format =~ /json/i) ? 'json' : ($format =~ /perl/i) ? 'perl' : do {
- warn "[!] Invalid format <<$format>> for option --dump\n";
- undef;
- }
- );
- },
- # Set a video player
- 'player|vplayer|video-player|video_player=s' => sub {
- if (not exists $opt{video_players}{$_[1]}) {
- die "[!] Unknown video player selected: <<$_[1]>>\n";
- }
- $opt{video_player_selected} = $_[1];
- },
- 'append-mplayer|append-arg|arg=s' => \$MPLAYER{user_defined_arguments},
- 'vo=s' => sub { $MPLAYER{video_output} = "-vo $_[1]" },
- 'af=s' => sub { $MPLAYER{audio_filter} = "-af $_[1]" },
- # Others
- 'colorful|colourful|C!' => \$opt{results_with_colors},
- 'details|D!' => \$opt{results_with_details},
- 'fixed-width|W|fw!' => \$opt{results_fixed_width},
- 'caption=s' => \$opt{videoCaption},
- 'fullscreen|fs|f!' => \$opt{fullscreen},
- 'dash!' => \$opt{dash_support},
- 'confirm!' => \$opt{confirm},
- 'prefer-mp4!' => \$opt{prefer_mp4},
- 'custom-layout!' => \$opt{custom_layout},
- 'custom-layout-format=s' => \$opt{custom_layout_format},
- 'merge-into-mkv|mkv-merge!' => \$opt{merge_into_mkv},
- 'merge-with-captions|merge-captions!' => \$opt{merge_with_captions},
- 'convert-command|convert-cmd=s' => \$opt{convert_cmd},
- 'dash-m4a|dash-mp4-audio|dash-mp4a!' => \$opt{dash_mp4_audio},
- 'wget-dl|wget-download!' => \$opt{download_with_wget},
- 'dl-parallel|download-in-parallel!' => \$opt{download_in_parallel},
- 'filename|filename-format=s' => \$opt{video_filename_format},
- 'rp|rem-played|remove-played-file!' => \$opt{remove_played_file},
- 'clobber!' => \$opt{clobber},
- 'info|i|video-info=s' => \$opt{print_video_info},
- 'get-term-width!' => \$opt{get_term_width},
- 'page=i' => \$opt{page},
- 'novideo|no-video|n!' => \$opt{novideo},
- 'autohide!' => \$opt{autohide_watched},
- 'highlight!' => \$opt{highlight_watched},
- 'results=i' => \$opt{maxResults},
- 'shuffle|s!' => \$opt{shuffle},
- 'more|m!' => \$opt{more_results},
- 'combine-multiple-videos|combine!' => \$opt{combine_multiple_videos},
- 'pos|position=i' => \$opt{position},
- 'ps|playlist-save=s' => \$opt{playlist_save},
- 'quiet|q!' => \$opt{quiet},
- 'really-quiet!' => \$opt{really_quiet},
- 'dp|downl-play|download-and-play|dl-play!' => \$opt{download_and_play},
- 'thousand-separator=s' => \$opt{thousand_separator},
- 'get-captions|get_captions!' => \$opt{get_captions},
- 'auto-captions|auto_captions!' => \$opt{auto_captions},
- 'copy-caption|copy_caption!' => \$opt{copy_caption},
- 'captions-dir|captions_dir=s' => \$opt{captions_dir},
- 'skip-if-exists|skip_if_exists!' => \$opt{skip_if_exists},
- 'downloads-dir|download-dir=s' => \$opt{downloads_dir},
- 'fat32safe!' => \$opt{fat32safe},
- )
- or warn "[!] Error in command-line arguments!\n";
- apply_configuration(\%opt, $keywords);
- }
- # Parse the arguments
- if (@ARGV) {
- require Encode;
- @ARGV = map { Encode::decode_utf8($_) } @ARGV;
- parse_arguments(\@ARGV);
- }
- for (my $i = 0 ; $i <= $#ARGV ; $i++) {
- my $arg = $ARGV[$i];
- next if chr ord $arg eq q{-};
- if (youtube_urls($arg)) {
- splice(@ARGV, $i--, 1);
- }
- }
- if (my @keywords = grep chr ord ne q{-}, @ARGV) {
- print_videos($yv_obj->search_videos(\@keywords));
- }
- elsif ($opt{interactive} and -t) {
- first_user_input();
- }
- elsif ($opt{interactive} and -t STDOUT and not -t) {
- print_videos($yv_obj->search_videos(scalar <STDIN>));
- }
- else {
- main_quit($opt{_error} || 0);
- }
- sub get_valid_video_id {
- my ($value) = @_;
- my $id =
- $value =~ /$get_video_id_re/ ? $+{video_id}
- : $value =~ /$valid_video_id_re/ ? $value
- : undef;
- unless (defined $id) {
- warn_invalid('videoID', $value);
- return;
- }
- return $id;
- }
- sub get_valid_playlist_id {
- my ($value) = @_;
- my $id =
- $value =~ /$get_playlist_id_re/ ? $+{playlist_id}
- : $value =~ /$valid_playlist_id_re/ ? $value
- : undef;
- unless (defined $id) {
- warn_invalid('playlistID', $value);
- return;
- }
- return $id;
- }
- sub apply_input_arguments {
- my ($args, $keywords) = @_;
- if (@{$args}) {
- local @ARGV = @{$args};
- parse_arguments($keywords);
- }
- return 1;
- }
- # Get mplayer
- sub get_mplayer {
- if ($constant{win32}) {
- my $smplayer = catfile($ENV{ProgramFiles}, qw(SMPlayer mplayer mplayer.exe));
- if (not -e $smplayer) {
- warn "\n\n!!! Please install SMPlayer in order to stream YouTube videos.\n\n";
- }
- return $smplayer; # Windows MPlayer
- }
- return 'mplayer'; # *NIX MPlayer
- }
- # Get term width
- sub get_term_width {
- return $term_width if $constant{win32};
- $term_width = (-t STDOUT) ? ((split(q{ }, `stty size`))[1] || $term_width) : $term_width;
- }
- sub first_user_input {
- my @keys = get_input_for_first_time();
- state $first_input_help = <<"HELP";
- $base_options
- $action_options
- $other_options
- $notes_options
- ** Example:
- To search for playlists, insert: -p keywords
- HELP
- if (scalar(@keys)) {
- my @for_search;
- foreach my $key (@keys) {
- if ($key =~ /$valid_opt_re/) {
- my $opt = $1;
- if (general_options(opt => $opt)) {
- ## ok
- }
- elsif ($opt =~ /^(?:h|help)\z/) {
- print $first_input_help;
- press_enter_to_continue();
- }
- elsif ($opt =~ /^(?:r|return)\z/) {
- return;
- }
- else {
- warn_invalid('option', $opt);
- print "\n";
- exit 1;
- }
- }
- elsif (youtube_urls($key)) {
- ## ok
- }
- else {
- push @for_search, $key;
- }
- }
- if (scalar(@for_search) > 0) {
- print_videos($yv_obj->search_videos(\@for_search));
- }
- else {
- __SUB__->();
- }
- }
- else {
- __SUB__->();
- }
- }
- sub get_quotewords {
- require Text::ParseWords;
- Text::ParseWords::quotewords(@_);
- }
- # Straight copy of parse_options() from Term::UI
- sub _parse_options {
- my ($input) = @_;
- my $return = {};
- while ( $input =~ s/(?:^|\s+)--?([-\w]+=(["']).+?\2)(?=\Z|\s+)//
- or $input =~ s/(?:^|\s+)--?([-\w]+=\S+)(?=\Z|\s+)//
- or $input =~ s/(?:^|\s+)--?([-\w]+)(?=\Z|\s+)//) {
- my $match = $1;
- if ($match =~ /^([-\w]+)=(["'])(.+?)\2$/) {
- $return->{$1} = $3;
- }
- elsif ($match =~ /^([-\w]+)=(\S+)$/) {
- $return->{$1} = $2;
- }
- elsif ($match =~ /^no-?([-\w]+)$/i) {
- $return->{$1} = 0;
- }
- elsif ($match =~ /^([-\w]+)$/) {
- $return->{$1} = 1;
- }
- }
- return wantarray ? ($return, $input) : $return;
- }
- sub parse_options2 {
- my ($input) = @_;
- warn(colored("\n[!] Input with an odd number of quotes: <$input>", 'bold red') . "\n\n")
- if $yv_obj->get_debug;
- my ($args, $keywords) = _parse_options($input);
- my @args =
- map $args->{$_} eq '0' ? "--no-$_"
- : $args->{$_} eq '1' ? "--$_"
- : "--$_=$args->{$_}" => keys %{$args};
- return wantarray ? (\@args, [split q{ }, $keywords]) : \@args;
- }
- sub parse_options {
- my ($input) = @_;
- my (@args, @keywords);
- if (not defined($input) or $input eq q{}) {
- return \@args, \@keywords;
- }
- foreach my $word (get_quotewords(qr/\s+/, 1, $input)) {
- if (chr ord $word eq q{-}) {
- push @args, $word;
- }
- else {
- push @keywords, $word;
- }
- }
- if (not @args and not @keywords) {
- return parse_options2($input);
- }
- return wantarray ? (\@args, \@keywords) : \@args;
- }
- sub get_user_input {
- my ($text) = @_;
- if (not $opt{interactive}) {
- if (not defined $opt{std_input}) {
- return ':return';
- }
- }
- my $input = unpack(
- 'A*', defined($opt{std_input})
- ? delete($opt{std_input})
- : ($term->readline($text) // return ':return')
- ) =~ s/^\s+//r;
- return q{:next} if $input eq q{}; # <ENTER> for the next page
- require Encode;
- $input = Encode::decode_utf8($input);
- my ($args, $keywords) = parse_options($input);
- if ($opt{history} and @{$keywords}) {
- my $str = join(' ', grep { /\w/ and not /^[:;=]/ } @{$keywords});
- if ($str ne '' and $str !~ /^[0-9]{1,2}\z/) {
- $term->append_history(1, $opt{history_file});
- }
- }
- apply_input_arguments($args, $keywords);
- return @{$keywords};
- }
- sub logout {
- unlink $authentication_file
- or warn "Can't unlink: `$authentication_file' -> $!";
- $yv_obj->set_access_token();
- $yv_obj->set_refresh_token();
- return 1;
- }
- sub authenticate {
- my $get_code_url = $yv_obj->get_accounts_oauth_url() // return;
- print <<"INFO";
- [*] Get the authentication code: $get_code_url
- |
- ... and paste it below. \\|/
- `
- INFO
- my $code = $term->readline(colored(q{Code: }, 'bold')) || return;
- my $info = $yv_obj->oauth_login($code) // do {
- warn "[WARNING] Can't log in... That's all I know...\n";
- return;
- };
- if (defined $info->{access_token}) {
- $yv_obj->set_access_token($info->{access_token}) // return;
- $yv_obj->set_refresh_token($info->{refresh_token}) // return;
- my $remember_me = ask_yn(prompt => colored("\nRemember me", 'bold'),
- default => 'y');
- if ($remember_me) {
- $yv_obj->set_authentication_file($authentication_file);
- $yv_obj->save_authentication_tokens()
- or warn "Can't store the authentication tokens: $!";
- }
- else {
- $yv_obj->set_authentication_file();
- }
- return 1;
- }
- warn "[WARNING] There was a problem with the authentication...\n";
- return;
- }
- sub authenticated {
- if (not defined $yv_obj->get_access_token) {
- warn_needs_auth();
- return;
- }
- return 1;
- }
- sub favorite_videos {
- my (@videoIDs) = @_;
- return if not authenticated();
- foreach my $id (@videoIDs) {
- my $videoID = get_valid_video_id($id) // next;
- if ($yv_obj->favorite_video($videoID)) {
- printf "\n[*] Video %s has been successfully favorited.\n", sprintf($CONFIG{youtube_video_url}, $videoID);
- }
- else {
- warn_cant_do('favorite', $videoID);
- }
- }
- return 1;
- }
- sub select_and_save_to_playlist {
- return if not authenticated();
- my $request = $yv_obj->my_playlists() // last;
- my $playlistID = print_playlists($request, return_playlist_id => 1);
- if (defined($playlistID)) {
- return save_to_playlist($playlistID, @_);
- }
- warn_no_thing_selected('playlist');
- return;
- }
- sub save_to_playlist {
- my ($playlistID, @videoIDs) = @_;
- return if not authenticated();
- foreach my $id (@videoIDs) {
- my $videoID = get_valid_video_id($id) // next;
- if ($yv_obj->add_video_to_playlist($playlistID, $videoID, $opt{position} || 1)) {
- printf("\n[*] Video %s has been successfully added to playlistID: %s\n",
- sprintf($CONFIG{youtube_video_url}, $videoID), $playlistID);
- }
- else {
- warn_cant_do("add to playlist", $videoID);
- }
- }
- return 1;
- }
- sub rate_videos {
- my $rating = shift;
- return if not authenticated();
- foreach my $id (@_) {
- my $videoID = get_valid_video_id($id) // next;
- if ($yv_obj->send_rating_to_video($videoID, $rating)) {
- print "\n[*] VideoID '$videoID' has been successfully ${rating}d.\n";
- }
- else {
- warn colored("\n[!] VideoID '$videoID' has not been ${rating}d", 'bold red') . "\n";
- }
- }
- return 1;
- }
- sub get_and_play_video_ids {
- my @ids = grep { get_valid_video_id($_) } @_;
- if (not @ids) {
- warn_invalid('video IDs', "@_");
- return;
- }
- my $info = $yv_obj->video_details(join(',', @ids), VIDEO_PART);
- if ($yv_utils->has_entries($info)) {
- if (not play_videos($info->{results}{items})) {
- return;
- }
- }
- else {
- warn_cant_do('get info about', "@ids");
- }
- return 1;
- }
- sub get_and_play_playlists {
- foreach my $id (@_) {
- my $videos = $yv_obj->videos_from_playlist_id(get_valid_playlist_id($id) // next);
- local $opt{play_all} = length($opt{std_input}) ? 0 : 1;
- print_videos($videos, auto => $opt{play_all});
- }
- return 1;
- }
- sub get_and_print_video_info {
- foreach my $id (@_) {
- my $videoID = get_valid_video_id($id) // next;
- my $info = $yv_obj->video_details($videoID, VIDEO_PART);
- if ($yv_utils->has_entries($info)) {
- print_video_info($info->{results}{items}[0]);
- }
- else {
- warn_cant_get('information', $videoID);
- }
- }
- return 1;
- }
- sub get_and_print_related_videos {
- foreach my $id (@_) {
- my $videoID = get_valid_video_id($id) // next;
- my $results = $yv_obj->related_to_videoID($videoID);
- print_videos($results);
- }
- return 1;
- }
- sub get_and_print_comments {
- foreach my $id (@_) {
- my $videoID = get_valid_video_id($id) // next;
- my $comments = $yv_obj->comments_from_video_id($videoID);
- print_comments($comments, $videoID);
- }
- return 1;
- }
- sub get_and_print_videos_from_playlist {
- my ($playlistID) = @_;
- if ($playlistID =~ /$valid_playlist_id_re/) {
- my $info = $yv_obj->videos_from_playlist_id($playlistID);
- if ($yv_utils->has_entries($info)) {
- print_videos($info);
- }
- else {
- warn colored("\n[!] Inexistent playlist...", 'bold red') . "\n";
- return;
- }
- }
- else {
- warn_invalid('playlistID', $playlistID);
- return;
- }
- return 1;
- }
- sub subscribe_to {
- my ($is_channel, @ids) = @_;
- return if not authenticated();
- foreach my $channel (@ids) {
- if ($channel =~ /$valid_channel_id_re/) {
- if (
- $is_channel
- ? $yv_obj->subscribe_channel($+{channel_id})
- : $yv_obj->subscribe_channel_from_username($+{channel_id})
- ) {
- print "[*] Successfully subscribed to channel: $channel\n";
- }
- else {
- warn colored("\n[!] Unable to subscribe to channel: $channel", 'bold red') . "\n";
- }
- }
- }
- return 1;
- }
- sub subscribe_to_channels {
- subscribe_to(1, @_);
- }
- sub subscribe_to_usernames {
- subscribe_to(0, @_);
- }
- sub _bold_color {
- my ($text) = @_;
- return colored($text, 'bold');
- }
- sub youtube_urls {
- my ($arg) = @_;
- if ($arg =~ /$get_video_id_re/) {
- get_and_play_video_ids($+{video_id});
- }
- elsif ($arg =~ /$get_playlist_id_re/) {
- get_and_print_videos_from_playlist($+{playlist_id});
- }
- elsif ($arg =~ /$get_channel_playlists_id_re/) {
- print_playlists($yv_obj->playlists($+{channel_id}));
- }
- elsif ($arg =~ /$get_channel_videos_id_re/) {
- print_videos($yv_obj->uploads($+{channel_id}));
- }
- elsif ($arg =~ /$get_username_playlists_re/) {
- print_playlists($yv_obj->playlists_from_username($+{username}));
- }
- elsif ($arg =~ /$get_username_videos_re/) {
- print_videos($yv_obj->uploads_from_username($+{username}));
- }
- else {
- return;
- }
- return 1;
- }
- sub general_options {
- my %args = @_;
- my $url = $args{url};
- my $option = $args{opt};
- my $callback = $args{sub};
- my $results = $args{res};
- my $info = $args{info};
- if (not defined($option)) {
- return;
- }
- if ($option =~ /^(?:q|quit|exit)\z/) {
- main_quit(0);
- }
- elsif ($option =~ /^(?:n|next)\z/ and defined $url) {
- if (defined $info->{nextPageToken}) {
- my $request = $yv_obj->next_page($url, $info->{nextPageToken});
- $callback->($request);
- }
- else {
- warn_last_page();
- }
- }
- elsif ($option =~ /^(?:R|refresh)\z/ and defined $url) {
- @{$results} = @{$yv_obj->_get_results($url)->{results}{items}};
- }
- elsif ($option =~ /^(?:b|back|p|prev|previous)\z/ and defined $url) {
- if (defined $info->{prevPageToken}) {
- my $request = $yv_obj->previous_page($url, $info->{prevPageToken});
- $callback->($request);
- }
- else {
- warn_first_page();
- }
- }
- elsif ($option eq 'login') {
- authenticate();
- }
- elsif ($option eq 'logout') {
- logout();
- }
- elsif ($option =~ /^(?:reset|reload|restart)\z/) {
- @ARGV = ();
- do $0;
- }
- elsif ($option =~ /^dv${digit_or_equal_re}(.*)/ and ref($results) eq 'ARRAY') {
- if (my @nums = get_valid_numbers($#{$results}, $1)) {
- print "\n";
- foreach my $num (@nums) {
- require Data::Dump;
- say Data::Dump::pp($results->[$num]);
- }
- press_enter_to_continue();
- }
- else {
- warn_no_thing_selected('result');
- }
- }
- elsif ($option =~ /^v(?:ideoids?)?=(.*)/) {
- if (my @ids = split(/[,\s]+/, $1)) {
- get_and_play_video_ids(@ids);
- }
- else {
- warn colored("\n[!] No video ID specified!", 'bold red') . "\n";
- }
- }
- elsif ($option =~ /^playlist(?:ID)?=(.*)/) {
- get_and_print_videos_from_playlist($1);
- }
- else {
- return;
- }
- return 1;
- }
- sub warn_no_results {
- warn colored("\n[!] No $_[0] results!", 'bold red') . "\n";
- }
- sub warn_invalid {
- my ($name, $option) = @_;
- warn colored("\n[!] Invalid $name: <$option>", 'bold red') . "\n";
- }
- sub warn_cant_do {
- my ($action, $videoID) = @_;
- warn colored("\n[!] Can't $action video: " . sprintf($CONFIG{youtube_video_url}, $videoID), 'bold red') . "\n";
- }
- sub warn_cant_get {
- my ($name, $videoID) = @_;
- warn colored("\n[!] Can't get $name for video: " . sprintf($CONFIG{youtube_video_url}, $videoID), 'bold red') . "\n";
- }
- sub warn_last_page {
- warn colored("\n[!] This is the last page!", "bold red") . "\n";
- }
- sub warn_first_page {
- warn colored("\n[!] No previous page available...", 'bold red') . "\n";
- }
- sub warn_no_thing_selected {
- warn colored("\n[!] No $_[0] selected!", 'bold red') . "\n";
- }
- sub warn_needs_auth {
- warn colored("\n[!] This functionality needs authentication!", 'bold red') . "\n";
- }
- # ... GET INPUT SUBS ... #
- sub get_input_for_first_time {
- return get_user_input(_bold_color("\n=>> Search for YouTube videos (:h for help)") . "\n> ");
- }
- sub get_input_for_channels {
- return get_user_input(_bold_color("\n=>> Select a channel (:h for help)") . "\n> ");
- }
- sub get_input_for_search {
- return get_user_input(_bold_color("\n=>> Select one or more videos to play (:h for help)") . "\n> ");
- }
- sub get_input_for_playlists {
- return get_user_input(_bold_color("\n=>> Select a playlist (:h for help)") . "\n> ");
- }
- sub get_input_for_comments {
- return get_user_input(_bold_color("\n=>> Press <ENTER> for the next page of comments (:h for help)") . "\n> ");
- }
- sub get_input_for_categories {
- return get_user_input(_bold_color("\n=>> Select a category (:h for help)") . "\n> ");
- }
- sub ask_yn {
- my (%opt) = @_;
- my $c = join('/', map { $_ eq $opt{default} ? ucfirst($_) : $_ } qw(y n));
- my $answ;
- do {
- $answ = lc($term->readline($opt{prompt} . " [$c]: "));
- $answ = $opt{default} unless $answ =~ /\S/;
- } while ($answ !~ /^y(?:es)?$/ and $answ !~ /^no?$/);
- return chr(ord($answ)) eq 'y';
- }
- sub get_reply {
- my (%opt) = @_;
- my $default = 1;
- while (my ($i, $choice) = each @{$opt{choices}}) {
- print "\n" if $i == 0;
- printf("%3d> %s\n", $i + 1, $choice);
- if ($choice eq $opt{default}) {
- $default = $i + 1;
- }
- }
- print "\n";
- my $answ;
- do {
- $answ = $term->readline($opt{prompt} . " [$default]: ");
- $answ = $default unless $answ =~ /\S/;
- } while ($answ !~ /^[0-9]+\z/ or $answ < 1 or $answ > @{$opt{choices}});
- return $opt{choices}[$answ - 1];
- }
- sub valid_num {
- my ($num, $array_ref) = @_;
- return $num =~ /^[0-9]{1,2}\z/ && $num != 0 && $num <= @{$array_ref};
- }
- sub adj_width {
- my ($str, $len, $prepend) = @_;
- $len > 0 or do {
- warn "[WARN] Insufficient space for the title: increase your terminal width!\n";
- return $str;
- };
- state $pkg = (
- eval {
- require Unicode::GCString;
- 'Unicode::GCString';
- } // eval {
- require Text::CharWidth;
- 'Text::CharWidth';
- } // do {
- warn "[WARN] Please install Unicode::GCString or Text::CharWidth in order to use this functionality.\n";
- '';
- }
- );
- #
- ## Unicode::GCString
- #
- if ($pkg eq 'Unicode::GCString') {
- my $gcstr = Unicode::GCString->new($str);
- my $str_width = $gcstr->columns;
- if ($str_width != $len) {
- while ($str_width > $len) {
- $gcstr = $gcstr->substr(0, -1);
- $str_width = $gcstr->columns;
- }
- $str = $gcstr->as_string;
- my $spaces = ' ' x ($len - $str_width);
- $str = $prepend ? "$spaces$str" : "$str$spaces";
- }
- return $str;
- }
- #
- ## Text::CharWidth
- #
- if ($pkg eq 'Text::CharWidth') {
- my $str_width = Text::CharWidth::mbswidth($str);
- if ($str_width != $len) {
- while ($str_width > $len) {
- chop $str;
- $str_width = Text::CharWidth::mbswidth($str);
- }
- my $spaces = ' ' x ($len - $str_width);
- $str = $prepend ? "$spaces$str" : "$str$spaces";
- }
- return $str;
- }
- return $str;
- }
- # ... PRINT SUBROUTINES ... #
- sub print_channels {
- my ($results) = @_;
- if (not $yv_utils->has_entries($results)) {
- warn_no_results("channel");
- }
- if ($opt{get_term_width} and $opt{results_fixed_width}) {
- get_term_width();
- }
- my $url = $results->{url};
- my $info = $results->{results} // {};
- my $channels = $info->{items} // [];
- my $i = 0;
- foreach my $channel (@{$channels}) {
- if ($opt{results_with_details}) {
- printf(
- "\n%s. %s\n %s: %-23s %s: %-12s\n%s\n",
- colored(sprintf('%2d', ++$i), 'bold') => colored($yv_utils->get_title($channel), 'bold blue'),
- colored('Updated' => 'bold') => $yv_utils->get_publication_date($channel),
- colored('Author' => 'bold') => $yv_utils->get_channel_title($channel),
- wrap_text(
- i_tab => q{ } x 4,
- s_tab => q{ } x 4,
- text => [$yv_utils->get_description($channel) || 'No description available...']
- ),
- );
- }
- elsif ($opt{results_fixed_width}) {
- require List::Util;
- my @authors = map { $yv_utils->get_channel_title($_) } @{$channels};
- my @dates = map { $yv_utils->get_publication_date($_) } @{$channels};
- my $author_width = List::Util::min(List::Util::max(map { length($_) } @authors), int($term_width / 5));
- my $dates_width = List::Util::max(map { length($_) } @dates);
- my $title_length = $term_width - ($author_width + $dates_width + 2 + 3 + 1 + 2);
- print "\n";
- foreach my $i (0 .. $#{$channels}) {
- my $channel = $channels->[$i];
- printf "%s. %s %s [%*s]\n", colored(sprintf('%2d', $i + 1), 'bold'),
- adj_width($yv_utils->get_title($channel), $title_length),
- adj_width($authors[$i], $author_width, 1),
- $dates_width, $dates[$i];
- }
- last;
- }
- else {
- print "\n" if $i == 0;
- printf "%s. %s (by %s)\n", colored(sprintf('%2d', ++$i), 'bold'), $yv_utils->get_title($channel),
- $yv_utils->get_channel_title($channel);
- }
- }
- my @keywords = get_input_for_channels();
- my @for_search;
- foreach my $key (@keywords) {
- if ($key =~ /$valid_opt_re/) {
- my $opt = $1;
- if (
- general_options(
- opt => $opt,
- sub => __SUB__,
- url => $url,
- res => $channels,
- info => $info,
- )
- ) {
- ## ok
- }
- elsif ($opt =~ /^(?:h|help)\z/) {
- print $general_help;
- press_enter_to_continue();
- }
- elsif ($opt =~ /^(?:r|return)\z/) {
- return;
- }
- else {
- warn_invalid('option', $opt);
- }
- }
- elsif (youtube_urls($key)) {
- ## ok
- }
- elsif (valid_num($key, $channels)) {
- print_videos($yv_obj->uploads($yv_utils->get_channel_id($channels->[$key - 1])));
- }
- else {
- push @for_search, $key;
- }
- }
- if (@for_search) {
- __SUB__->($yv_obj->search_channels(\@for_search));
- }
- __SUB__->(@_);
- }
- sub print_comments {
- my ($results, $videoID) = @_;
- if (not $yv_utils->has_entries($results)) {
- warn_no_results("comments");
- }
- my $url = $results->{url};
- my $info = $results->{results} // {};
- my $comments = $info->{items} // [];
- my $i = 0;
- foreach my $comment (@{$comments}) {
- my $snippet = (($comment->{snippet} // next)->{topLevelComment} // next)->{snippet};
- printf(
- "\n%s on %s said:\n%s\n",
- colored($snippet->{authorDisplayName}, 'bold'),
- $yv_utils->format_date($snippet->{publishedAt}),
- wrap_text(
- i_tab => q{ } x 4,
- s_tab => q{ } x 4,
- text => [$snippet->{textDisplay} // 'Empty comment...']
- ),
- );
- }
- my @keywords = get_input_for_comments();
- foreach my $key (@keywords) {
- if ($key =~ /$valid_opt_re/) {
- my $opt = $1;
- if (
- general_options(
- opt => $opt,
- sub => __SUB__,
- url => $url,
- res => $comments,
- info => $info,
- mode => 'comments',
- args => [$videoID],
- )
- ) {
- ## ok
- }
- elsif ($opt =~ /^(?:h|help)\z/) {
- print $comments_help;
- press_enter_to_continue();
- }
- elsif ($opt =~ /^(?:c|comment)\z/) {
- if (authenticated()) {
- require File::Temp;
- my ($fh, $filename) = File::Temp::tempfile();
- $yv_obj->proxy_system($ENV{EDITOR} // 'nano', $filename);
- if ($?) {
- warn colored("\n[!] Editor exited with a non-zero code. Unable to continue!", 'bold red') . "\n";
- }
- else {
- my $comment = do { local (@ARGV, $/) = $filename; <> };
- $comment =~ s/[^\s[:^cntrl:]]+//g; # remove control characters
- if (length($comment) and $yv_obj->comment_to_video_id($comment, $videoID)) {
- print "\n[*] Comment posted!\n";
- }
- else {
- warn colored("\n[!] Your comment has NOT been posted!", 'bold red') . "\n";
- }
- }
- }
- }
- elsif ($opt =~ /^(?:r|return)\z/) {
- return;
- }
- else {
- warn_invalid('option', $opt);
- }
- }
- elsif (youtube_urls($key)) {
- ## ok
- }
- elsif (valid_num($key, $comments)) {
- print_videos($yv_obj->get_videos_from_username($comments->[$key - 1]{author}));
- }
- else {
- warn_invalid('keyword', $key);
- }
- }
- __SUB__->(@_);
- }
- sub print_categories {
- my ($results) = @_;
- my $categories = $results->{items};
- return if ref $categories ne 'ARRAY';
- my $i = 0;
- print "\n" if @{$categories};
- foreach my $category (@{$categories}) {
- # Ignore nonassignable categories
- $category->{snippet}{assignable} || next;
- printf "%s. %-40s (id: %s)\n", colored(sprintf('%2d', ++$i), 'bold'), $yv_utils->get_title($category), $category->{id};
- }
- my @keywords = get_input_for_categories();
- foreach my $key (@keywords) {
- if ($key =~ /$valid_opt_re/) {
- my $opt = $1;
- if (
- general_options(
- opt => $opt,
- sub => __SUB__,
- res => $results,
- )
- ) {
- ## ok
- }
- elsif ($opt =~ /^(?:h|help)\z/) {
- print $general_help;
- press_enter_to_continue();
- }
- elsif ($opt =~ /^(?:r|return)\z/) {
- return;
- }
- else {
- warn_invalid('option', $opt);
- }
- }
- elsif (youtube_urls($key)) {
- ## ok
- }
- elsif (valid_num($key, $categories)) {
- my $cat_id = $categories->[$key - 1]{id};
- print_videos($yv_obj->videos_from_category($cat_id));
- }
- else {
- warn_invalid('keyword', $key);
- }
- }
- __SUB__->(@_);
- }
- sub print_playlists {
- my ($results, %args) = @_;
- if (not $yv_utils->has_entries($results)) {
- warn_no_results("playlist");
- }
- if ($opt{get_term_width} and $opt{results_fixed_width}) {
- get_term_width();
- }
- my $url = $results->{url};
- my $info = $results->{results} // {};
- my $playlists = $info->{items} // [];
- if ($opt{shuffle}) {
- require List::Util;
- $playlists = [List::Util::shuffle(@{$playlists})];
- }
- state $info_format = <<"FORMAT";
- TITLE: %s
- ID: %s
- URL: https://www.youtube.com/playlist?list=%s
- DESCR: %s
- FORMAT
- foreach my $i (0 .. $#{$playlists}) {
- my $playlist = $playlists->[$i];
- if ($opt{results_with_details}) {
- printf(
- "\n%s. %s\n %s: %-25s %s: %s\n%s\n",
- colored(sprintf('%2d', $i + 1), 'bold') => colored($yv_utils->get_title($playlist), 'bold blue'),
- colored('Updated' => 'bold') => $yv_utils->get_publication_date($playlist),
- colored('Author' => 'bold') => $yv_utils->get_channel_title($playlist),
- wrap_text(
- i_tab => q{ } x 4,
- s_tab => q{ } x 4,
- text => [$yv_utils->get_description($playlist) || 'No description available...']
- ),
- );
- }
- elsif ($opt{results_fixed_width}) {
- require List::Util;
- my @authors = map { $yv_utils->get_channel_title($_) } @{$playlists};
- my @dates = map { $yv_utils->get_publication_date($_) } @{$playlists};
- my $author_width = List::Util::min(List::Util::max(map { length($_) } @authors), int($term_width / 5));
- my $dates_width = List::Util::max(map { length($_) } @dates);
- my $title_length = $term_width - ($author_width + $dates_width + 2 + 3 + 1 + 2);
- print "\n";
- foreach my $i (0 .. $#{$playlists}) {
- my $playlist = $playlists->[$i];
- printf "%s. %s %s [%*s]\n", colored(sprintf('%2d', $i + 1), 'bold'),
- adj_width($yv_utils->get_title($playlist), $title_length),
- adj_width($authors[$i], $author_width, 1),
- $dates_width, $dates[$i];
- }
- last;
- }
- elsif ($opt{results_with_colors}) {
- print "\n" if $i == 0;
- printf(
- "%s. %s (%s) [%s]\n",
- colored(sprintf('%2d', $i + 1), 'bold'),
- colored($yv_utils->get_title($playlist), 'bold green'),
- colored("by " . $yv_utils->get_channel_title($playlist), 'bold yellow'),
- colored($yv_utils->get_publication_date($playlist), 'bold blue'),
- );
- }
- else {
- print "\n" if $i == 0;
- printf(
- "%s. %s (by %s) [%s]\n",
- colored(sprintf('%2d', $i + 1), 'bold'), $yv_utils->get_title($playlist),
- $yv_utils->get_channel_title($playlist), $yv_utils->get_publication_date($playlist)
- );
- }
- }
- state @keywords;
- if ($args{auto}) { } # do nothing...
- else {
- @keywords = get_input_for_playlists();
- if (scalar(@keywords) == 0) {
- __SUB__->(@_);
- }
- }
- my $contains_keywords = grep /$non_digit_or_opt_re/, @keywords;
- my @for_search;
- foreach my $key (@keywords) {
- if ($key =~ /$valid_opt_re/) {
- my $opt = $1;
- if (
- general_options(
- opt => $opt,
- sub => __SUB__,
- url => $url,
- res => $playlists,
- info => $info,
- mode => 'playlists',
- )
- ) {
- ## ok
- }
- elsif ($opt =~ /^(?:h|help)\z/) {
- print $playlists_help;
- press_enter_to_continue();
- }
- elsif ($opt =~ /^(?:r|return)\z/) {
- return;
- }
- elsif ($opt =~ /^i(?:nfo)?${digit_or_equal_re}(.*)/) {
- if (my @ids = get_valid_numbers($#{$playlists}, $1)) {
- foreach my $id (@ids) {
- my $desc = wrap_text(
- i_tab => q{ } x 7,
- s_tab => q{ } x 7,
- text => [$yv_utils->get_description($playlists->[$id]) || 'No description available...']
- );
- $desc =~ s/^\s+//;
- printf $info_format, $yv_utils->get_title($playlists->[$id]),
- ($yv_utils->get_playlist_id($playlists->[$id])) x 2, $desc;
- }
- press_enter_to_continue();
- }
- else {
- warn_no_thing_selected('playlist');
- }
- }
- elsif ($opt =~ /^pp${digit_or_equal_re}(.*)/) {
- if (my @ids = get_valid_numbers($#{$playlists}, $1)) {
- my $arg = "--pp=" . join(q{,}, map { $yv_utils->get_playlist_id($_) } @{$playlists}[@ids]);
- apply_input_arguments([$arg]);
- }
- else {
- warn_no_thing_selected('playlist');
- }
- }
- else {
- warn_invalid('option', $opt);
- }
- }
- elsif (youtube_urls($key)) {
- ## ok
- }
- elsif (valid_num($key, $playlists) and not $contains_keywords) {
- if ($args{return_playlist_id}) {
- return $yv_utils->get_playlist_id($playlists->[$key - 1]);
- }
- get_and_print_videos_from_playlist($yv_utils->get_playlist_id($playlists->[$key - 1]));
- }
- else {
- push @for_search, $key;
- }
- }
- if (@for_search) {
- __SUB__->($yv_obj->search_playlists(\@for_search));
- }
- __SUB__->(@_);
- }
- sub compile_regex {
- my ($value) = @_;
- $value =~ s{^(?<quote>['"])(?<regex>.+)\g{quote}$}{$+{regex}}s;
- my $re = eval { use re qw(eval); qr/$value/i };
- if ($@) {
- warn_invalid("regex", $@);
- return;
- }
- return $re;
- }
- sub get_range_numbers {
- my ($first, $second) = @_;
- return (
- $first > $second
- ? (reverse($second .. $first))
- : ($first .. $second)
- );
- }
- sub get_valid_numbers {
- my ($max, $input) = @_;
- my @output;
- foreach my $id (split(/[,\s]+/, $input)) {
- push @output,
- $id =~ /$range_num_re/ ? get_range_numbers($1, $2)
- : $id =~ /^[0-9]{1,2}\z/ ? $id
- : next;
- }
- return grep { $_ >= 0 and $_ <= $max } map { $_ - 1 } @output;
- }
- sub get_streaming_url {
- my ($video_id) = @_;
- my ($urls, $captions, $info) = $yv_obj->get_streaming_urls($video_id);
- if (not defined $urls) {
- return scalar {};
- }
- # Download the closed-captions
- my $srt_file;
- if ( ref($captions) eq 'ARRAY'
- and @$captions
- and $opt{get_captions}
- and not $opt{novideo}) {
- require WWW::YoutubeViewer::GetCaption;
- my $yv_cap = WWW::YoutubeViewer::GetCaption->new(
- auto_captions => $opt{auto_captions},
- captions_dir => $opt{captions_dir},
- captions => $captions,
- languages => $CONFIG{srt_languages},
- );
- $srt_file = $yv_cap->save_caption($video_id);
- }
- require WWW::YoutubeViewer::Itags;
- state $yv_itags = WWW::YoutubeViewer::Itags->new();
- # Include DASH itags
- my $dash = 1;
- # Exclude DASH itags in download-mode or when no video output is required
- if ($opt{novideo} or not $opt{dash_support}) {
- $dash = 0;
- }
- elsif ($opt{download_video}) {
- $dash = $opt{merge_into_mkv} ? 1 : 0;
- }
- my ($streaming, $resolution) =
- $yv_itags->find_streaming_url(
- urls => $urls,
- resolution => $opt{resolution},
- dash => $dash,
- dash_mp4_audio => $opt{dash_mp4_audio},
- );
- return {
- streaming => $streaming,
- srt_file => $srt_file,
- info => $info,
- resolution => $resolution,
- };
- }
- sub download_from_url {
- my ($url, $output_filename) = @_;
- my $i = 0;
- while (-e $output_filename and not $opt{clobber} and ++$i) {
- my $last_i = $i > 1 ? $i - 1 : q{/};
- $output_filename =~ s{(?:_$last_i)?(\.\w{3,4})$}{_$i$1};
- }
- # Download video with wget
- if ($opt{download_with_wget}) {
- my @cmd = ($opt{wget_cmd}, ($opt{clobber} ? () : q{-nc}), $url, q{-O}, $output_filename);
- if ($opt{download_in_parallel}) {
- my $pid = fork() // warn "[ERROR] Can't fork: $!";
- if ($pid == 0) {
- $yv_obj->proxy_exec(@cmd, '--quiet');
- }
- }
- else {
- $yv_obj->proxy_system(@cmd);
- return if $?;
- }
- }
- # Download video with LWP::UserAgent
- else {
- require LWP::UserAgent;
- my $lwp = LWP::UserAgent->new(
- show_progress => 1,
- agent =>
- 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36',
- );
- $lwp->proxy(['http', 'https'], $yv_obj->get_http_proxy)
- if defined($yv_obj->get_http_proxy);
- if ($opt{download_in_parallel}) {
- my $pid = fork() // warn "[ERROR] Can't fork: $!";
- if ($pid == 0) {
- $lwp->show_progress(0);
- $lwp->mirror($url, $output_filename);
- exit;
- }
- }
- else {
- my $resp = eval { $lwp->mirror($url, $output_filename) };
- if ($@ =~ /\bread timeout\b/i or not defined($resp) or not $resp->is_success) {
- warn colored("\n[!] Encountered an error while downloading... Trying again...", 'bold red') . "\n\n";
- if (-x '/usr/bin/wget') {
- $CONFIG{download_with_wget} = 1;
- dump_configuration();
- }
- else {
- warn colored("[!] Please install `wget` and try again...", 'bold red') . "\n\n";
- }
- unlink($output_filename);
- $opt{download_with_wget} = 1;
- return download_from_url($url, $output_filename);
- }
- }
- }
- return $output_filename;
- }
- sub download_video {
- my ($streaming, $info) = @_;
- my $fat32safe = $opt{fat32safe};
- state $unix_like = $^O =~ /^(?:linux|freebsd|openbsd)\z/i;
- if (not $fat32safe and not $unix_like) {
- $fat32safe = 1;
- }
- my $video_filename = $yv_utils->format_text(
- streaming => $streaming,
- info => $info,
- text => $opt{video_filename_format},
- escape => 0,
- fat32safe => $fat32safe,
- );
- my $audio_filename;
- my $naked_filename = $video_filename =~ s/\.\w+\z//r;
- my $mkv_filename = "$naked_filename.mkv";
- my $srt_filename = "$naked_filename.srt";
- my $video_info = $streaming->{streaming};
- my $audio_info = $streaming->{streaming}{__AUDIO__};
- if (not -d $opt{downloads_dir}) {
- require File::Path;
- unless (File::Path::make_path($opt{downloads_dir})) {
- warn colored("\n[!] Can't create directory '$opt{downloads_dir}': $1", 'bold red') . "\n";
- }
- }
- if (not -w $opt{downloads_dir}) {
- warn colored("\n[!] Can't write into directory '$opt{downloads_dir}': $!", 'bold red') . "\n";
- $opt{downloads_dir} = (-w curdir()) ? curdir() : (-w $ENV{HOME}) ? $ENV{HOME} : return;
- warn colored("[!] Video will be downloaded into directory: $opt{downloads_dir}", 'bold red') . "\n";
- }
- $video_filename = catfile($opt{downloads_dir}, $video_filename);
- if ($opt{skip_if_exists} and -e $video_filename) {
- say "[*] File `$video_filename` already exists. Skipping...";
- }
- elsif ($opt{skip_if_exists} and -e $mkv_filename) {
- $video_filename = $mkv_filename;
- say "[*] File `$mkv_filename` already exists. Skipping...";
- }
- else {
- # Disable `download in parallel` in combination with `download and play`
- if ($opt{download_in_parallel} and $opt{download_and_play}) {
- warn colored("[!] Downloading in parallel is not supported with `--dl-play`...", 'bold red') . "\n";
- $opt{download_in_parallel} = 0;
- }
- # Disable `download in parallel` in combination with `merge into mkv`
- if ($opt{download_in_parallel} and $opt{merge_into_mkv}) {
- warn colored("[!] Downloading in parallel is not supported with `--mkv-merge`...", 'bold red') . "\n";
- $opt{download_in_parallel} = 0;
- }
- $video_filename = download_from_url($video_info->{url}, $video_filename) // return;
- $audio_filename =
- download_from_url($audio_info->{url}, $naked_filename . ' - audio.' . $yv_utils->extension($audio_info->{type}))
- if $audio_info;
- }
- my @merge_files = ($video_filename);
- if (defined($audio_filename)) {
- push @merge_files, $audio_filename;
- }
- if (defined($streaming->{srt_file}) and $opt{merge_with_captions}) {
- push @merge_files, $streaming->{srt_file};
- }
- if ($opt{merge_into_mkv} and @merge_files > 1 and not -e $mkv_filename) {
- say "[*] Merging into MKV...";
- my $merge_command =
- join(' ', $opt{ffmpeg_cmd}, (map { "-i \Q$_\E" } @merge_files), $opt{merge_into_mkv_args}, "\Q$mkv_filename\E");
- if ($yv_obj->get_debug) {
- say "-> Command: $merge_command";
- }
- $yv_obj->proxy_system($merge_command);
- if ($? == 0 and -e $mkv_filename) {
- unlink @merge_files;
- $video_filename = $mkv_filename;
- }
- }
- # Convert the downloaded video
- if (defined $opt{convert_to}) {
- my $convert_filename = "$naked_filename.$opt{convert_to}";
- my $convert_cmd = $opt{convert_cmd};
- my %table = (
- 'IN' => $video_filename,
- 'OUT' => $convert_filename,
- );
- my $regex = do {
- local $" = '|';
- qr/\*(@{[keys %table]})\*/;
- };
- $convert_cmd =~ s/$regex/\Q$table{$1}\E/g;
- say $convert_cmd if $yv_obj->get_debug;
- $yv_obj->proxy_system($convert_cmd);
- if ($? == 0) {
- if (not $opt{keep_original_video}) {
- unlink $video_filename
- or warn colored("\n[!] Can't unlink file '$video_filename': $!", 'bold red') . "\n\n";
- }
- $video_filename = $convert_filename if -e $convert_filename;
- }
- }
- # Play the download video
- if ($opt{download_and_play}) {
- local $streaming->{streaming}{url} = '';
- local $streaming->{streaming}{__AUDIO__} = undef;
- local $streaming->{srt_file} = undef if ($opt{merge_into_mkv} && $opt{merge_with_captions});
- my $command = get_player_command($streaming, $info);
- say "-> Command: ", $command if $yv_obj->get_debug;
- $yv_obj->proxy_system(join(q{ }, $command, quotemeta($video_filename)));
- # Remove it afterwards
- if ($? == 0 and $opt{remove_played_file}) {
- unlink $video_filename
- or warn colored("\n[!] Can't unlink file '$video_filename': $!", 'bold red') . "\n\n";
- }
- }
- # Copy the .srt file from captions-dir to downloads-dir
- if ( $opt{copy_caption}
- and -e $video_filename
- and defined($streaming->{srt_file})
- and -e $streaming->{srt_file}) {
- my $from = $streaming->{srt_file};
- my $to = catfile($opt{downloads_dir}, $srt_filename);
- require File::Copy;
- File::Copy::cp($from, $to);
- }
- return 1;
- }
- sub get_player_command {
- my ($streaming, $video) = @_;
- $MPLAYER{fullscreen} = $opt{fullscreen} ? $opt{video_players}{$opt{video_player_selected}}{fs} // '' : q{};
- $MPLAYER{novideo} = $opt{novideo} ? $opt{video_players}{$opt{video_player_selected}}{novideo} // '' : q{};
- $MPLAYER{mplayer_arguments} = $opt{video_players}{$opt{video_player_selected}}{arg} // q{};
- my $cmd = join(
- q{ },
- (
- # Video player
- $opt{video_players}{$opt{video_player_selected}}{cmd},
- ( # Audio file (https://)
- ref($streaming->{streaming}{__AUDIO__}) eq 'HASH'
- && exists($opt{video_players}{$opt{video_player_selected}}{audio})
- ? $opt{video_players}{$opt{video_player_selected}}{audio}
- : ()
- ),
- ( # Subtitle file (.srt)
- defined($streaming->{srt_file})
- && exists($opt{video_players}{$opt{video_player_selected}}{srt})
- ? $opt{video_players}{$opt{video_player_selected}}{srt}
- : ()
- ),
- # Rest of the arguments
- grep({ defined($_) and /\S/ } values %MPLAYER)
- )
- );
- my $has_video = $cmd =~ /\*(?:VIDEO|URL|ID)\*/;
- $cmd = $yv_utils->format_text(
- streaming => $streaming,
- info => $video,
- text => $cmd,
- escape => 1,
- );
- $has_video ? $cmd : join(' ', $cmd, quotemeta($streaming->{streaming}{url}));
- }
- sub play_videos {
- my ($videos) = @_;
- my @streaming_urls;
- foreach my $video (@{$videos}) {
- my $video_id = $yv_utils->get_video_id($video);
- # It may be downloaded, but that's OK...
- if ($opt{highlight_watched}) {
- $watched_videos{$video_id} = 1;
- }
- if (defined($opt{max_seconds}) and $opt{max_seconds} >= 0) {
- next if $yv_utils->get_duration($video) > $opt{max_seconds};
- }
- if (defined($opt{min_seconds}) and $opt{min_seconds} >= 0) {
- next if $yv_utils->get_duration($video) < $opt{min_seconds};
- }
- my $streaming = get_streaming_url($video_id);
- if (ref($streaming->{streaming}) ne 'HASH') {
- warn colored("[!] No streaming URL has been found...", 'bold red') . "\n";
- next;
- }
- if ( !defined($streaming->{streaming}{url})
- and defined($streaming->{info}{status})
- and $streaming->{info}{status} =~ /(?:error|fail)/i) {
- warn colored("[!] Error on: ", 'bold red') . sprintf($CONFIG{youtube_video_url}, $video_id) . "\n";
- warn colored("[*] Reason: ", 'bold red') . $streaming->{info}{reason} =~ tr/+/ /r . "\n\n";
- }
- # Dump metadata information
- if (defined($opt{dump})) {
- my $file = $video_id . '.' . $opt{dump};
- open(my $fh, '>:utf8', $file)
- or die "Can't open file `$file' for writing: $!";
- local $video->{streaming} = $streaming;
- if ($opt{dump} eq 'json') {
- print {$fh} JSON->new->pretty(1)->encode($video);
- }
- elsif ($opt{dump} eq 'perl') {
- require Data::Dump;
- print {$fh} Data::Dump::pp($video);
- }
- close $fh;
- }
- if ($opt{download_video}) {
- print_video_info($video);
- if (not download_video($streaming, $video)) {
- return;
- }
- }
- elsif (length($opt{extract_info})) {
- my $fh = $opt{extract_info_fh} // \*STDOUT;
- say {$fh}
- $yv_utils->format_text(
- streaming => $streaming,
- info => $video,
- text => $opt{extract_info},
- escape => $opt{escape_info},
- fat32safe => $opt{fat32safe},
- );
- }
- elsif ($opt{combine_multiple_videos}) {
- print_video_info($video);
- push @streaming_urls, $streaming;
- }
- else {
- print_video_info($video);
- my $command = get_player_command($streaming, $video);
- if ($yv_obj->get_debug) {
- say "-> Resolution: $streaming->{resolution}";
- say "-> Video itag: $streaming->{streaming}{itag}";
- say "-> Audio itag: $streaming->{streaming}{__AUDIO__}{itag}" if exists $streaming->{streaming}{__AUDIO__};
- say "-> Video type: $streaming->{streaming}{type}";
- say "-> Audio type: $streaming->{streaming}{__AUDIO__}{type}" if exists $streaming->{streaming}{__AUDIO__};
- say "-> Command: $command";
- }
- $yv_obj->proxy_system($command); # execute the video player
- if ($? and $? != 512) {
- $opt{auto_next_page} = 0;
- return;
- }
- }
- press_enter_to_continue() if $opt{confirm};
- }
- if ($opt{combine_multiple_videos} && @streaming_urls) {
- my $streaming = $streaming_urls[0];
- my $command = get_player_command($streaming, $videos->[0]);
- say $command if $yv_obj->get_debug;
- $yv_obj->proxy_system(join(q{ }, $command, map { quotemeta($_->{streaming}{url}) } @streaming_urls));
- return if $?;
- }
- return 1;
- }
- sub play_videos_matched_by_regex {
- my %args = @_;
- my $key = $args{key};
- my $regex = $args{regex};
- my $videos = $args{videos};
- my $sub = \&{'WWW::YoutubeViewer::Utils' . '::' . 'get_' . $key};
- if (not defined &$sub) {
- warn colored("\n[!] Invalid key: <$key>.", 'bold red') . "\n";
- return;
- }
- if (defined(my $re = compile_regex($regex))) {
- if (my @nums = grep { $yv_utils->$sub($videos->[$_]) =~ /$re/ } 0 .. $#{$videos}) {
- if (not play_videos([@{$videos}[@nums]])) {
- return;
- }
- }
- else {
- warn colored("\n[!] No video <$key> matched by the regex: $re", 'bold red') . "\n";
- return;
- }
- }
- return 1;
- }
- sub print_video_info {
- my ($video) = @_;
- my $hr = '-' x ($opt{get_term_width} ? get_term_width() : $term_width);
- printf(
- "\n%s %s\n%s\n%s\n%s\n%s",
- _bold_color('=>>'),
- 'Description',
- $hr,
- wrap_text(
- i_tab => q{},
- s_tab => q{},
- text => [$yv_utils->get_description($video) || 'No description available...']
- ),
- $hr,
- _bold_color('* URL: ')
- );
- print STDOUT sprintf($CONFIG{youtube_video_url}, $yv_utils->get_video_id($video));
- my $title = $yv_utils->get_title($video);
- my $title_length = length($title);
- my $rep = ($term_width - $title_length) / 2 - 4;
- $rep = 0 if $rep < 0;
- print "\n$hr\n", q{ } x $rep => (_bold_color("=>> $title <<=") . "\n\n"),
- map(sprintf(q{-> } . "%-*s: %s\n", $opt{_colors} ? 18 : 10, _bold_color($_->[0]), $_->[1]),
- (
- ['Channel' => $yv_utils->get_channel_title($video)],
- ['ChannelID' => $yv_utils->get_channel_id($video)],
- ['Category' => $yv_utils->get_category_name($video)],
- ['Definition' => $yv_utils->get_definition($video)],
- ['Duration' => $yv_utils->format_time($yv_utils->get_duration($video))],
- ['Likes' => $yv_utils->set_thousands($yv_utils->get_likes($video))],
- ['Dislikes' => $yv_utils->set_thousands($yv_utils->get_dislikes($video))],
- ['Comments' => $yv_utils->set_thousands($yv_utils->get_comments($video))],
- ['Views' => $yv_utils->set_thousands($yv_utils->get_views($video))],
- ['Published' => $yv_utils->get_publication_date($video)],
- )),
- "$hr\n";
- return 1;
- }
- sub print_videos {
- my ($results, %args) = @_;
- if (not $yv_utils->has_entries($results)) {
- warn_no_results("video");
- }
- if ($opt{get_term_width} and $opt{results_fixed_width}) {
- get_term_width();
- }
- my $url = $results->{url};
- my $info = $results->{results} // {};
- my $videos = $info->{items} // [];
- #<<<
- @$videos = grep {
- ref($_) eq 'HASH' && ref($_->{id}) eq 'HASH'
- ? (exists($_->{id}{kind})
- ? $_->{id}{kind} eq 'youtube#video'
- : 0)
- : 1
- } @$videos;
- #>>>
- if ($opt{shuffle}) {
- require List::Util;
- $videos = [List::Util::shuffle(@{$videos})];
- }
- if (@{$videos} and not $results->{has_extra_info}) {
- my $content_details = $yv_obj->video_details(join(',', map { $yv_utils->get_video_id($_) } @{$videos}), VIDEO_PART);
- my $video_details = $content_details->{results}{items};
- foreach my $i (0 .. $#{$videos}) {
- @{$videos->[$i]}{qw(id contentDetails statistics snippet)} =
- @{$video_details->[$i]}{qw(id contentDetails statistics snippet)};
- }
- $results->{has_extra_info} = 1;
- }
- #<<<
- # Filter out private or deleted videos
- @$videos = grep {
- defined($yv_utils->get_video_id($_))
- } @$videos;
- #>>>
- my @formatted;
- foreach my $i (0 .. $#{$videos}) {
- my $video = $videos->[$i];
- if ($opt{custom_layout}) {
- my $entry = $opt{custom_layout_format};
- $entry =~ s/\*NO\*/sprintf('%2d', $i+1)/ge;
- $entry = $yv_utils->format_text(
- info => $video,
- text => $entry,
- escape => 0,
- );
- push @formatted, $entry;
- }
- elsif ($opt{results_with_details}) {
- push @formatted,
- ($i == 0 ? '' : "\n")
- . sprintf(
- "%s. %s\n" . " %s: %-16s %s: %-13s %s: %s\n" . " %s: %-12s %s: %-10s %s: %s\n%s\n",
- colored(sprintf('%2d', $i + 1), 'bold') => colored($yv_utils->get_title($video), 'bold blue'),
- colored('Views' => 'bold') => $yv_utils->set_thousands($yv_utils->get_views($video)),
- colored('Likes' => 'bold') => $yv_utils->set_thousands($yv_utils->get_likes($video)),
- colored('Dislikes' => 'bold') => $yv_utils->set_thousands($yv_utils->get_dislikes($video)),
- colored('Published' => 'bold') => $yv_utils->get_publication_date($video),
- colored('Duration' => 'bold') => $yv_utils->format_time($yv_utils->get_duration($video)),
- colored('Author' => 'bold') => $yv_utils->get_channel_title($video),
- wrap_text(
- i_tab => q{ } x 4,
- s_tab => q{ } x 4,
- text => [$yv_utils->get_description($video) || 'No description available...']
- ),
- );
- }
- elsif ($opt{results_with_colors}) {
- my $definition = $yv_utils->get_definition($video);
- push @formatted,
- sprintf(
- "%s. %s (%s) [%s]\n",
- colored(sprintf('%2d', $i + 1), 'bold'),
- colored($yv_utils->get_title($video), 'bold green'),
- colored("by " . $yv_utils->get_channel_title($video), 'bold yellow'),
- colored($yv_utils->format_time($yv_utils->get_duration($video)), 'bold bright_blue'),
- );
- }
- elsif ($opt{results_fixed_width}) {
- require List::Util;
- my @durations = map { $yv_utils->get_duration($_) } @{$videos};
- my @authors = map { $yv_utils->get_channel_title($_) } @{$videos};
- my $author_width = List::Util::min(List::Util::max(map { length($_) } @authors) || 1, int($term_width / 5));
- my $time_width = List::Util::first(sub { $_ >= 3600 }, @durations) ? 8 : 6;
- my $title_length = $term_width - ($author_width + $time_width + 3 + 2 + 1);
- foreach my $i (0 .. $#{$videos}) {
- my $video = $videos->[$i];
- push @formatted,
- sprintf("%s. %s %s %*s\n",
- colored(sprintf('%2d', $i + 1), 'bold'),
- adj_width($yv_utils->get_title($video), $title_length),
- adj_width($yv_utils->get_channel_title($video), $author_width, 1),
- $time_width,
- $yv_utils->format_time($durations[$i]));
- }
- last;
- }
- else {
- push @formatted,
- sprintf(
- "%s. %s (by %s) [%s]\n",
- colored(sprintf('%2d', $i + 1), 'bold'), $yv_utils->get_title($video),
- $yv_utils->get_channel_title($video), $yv_utils->format_time($yv_utils->get_duration($video)),
- );
- }
- }
- if ($opt{highlight_watched}) {
- foreach my $i (0 .. $#{$videos}) {
- my $video = $videos->[$i];
- if (exists($watched_videos{$yv_utils->get_video_id($video)})) {
- $formatted[$i] = colored(colorstrip($formatted[$i]), $opt{highlight_color});
- }
- }
- }
- if (@formatted) {
- print "\n" . join("", @formatted);
- }
- if ($opt{play_all} || $opt{play_backwards}) {
- if (@{$videos}) {
- if (
- play_videos(
- $opt{play_backwards}
- ? [reverse @{$videos}]
- : $videos
- )
- ) {
- if ($opt{play_backwards}) {
- if (defined $info->{prevPageToken}) {
- __SUB__->($yv_obj->previous_page($url, $info->{prevPageToken}), auto => 1);
- }
- else {
- $opt{play_backwards} = 0;
- warn_first_page();
- return;
- }
- }
- else {
- if (defined $info->{nextPageToken}) {
- __SUB__->($yv_obj->next_page($url, $info->{nextPageToken}), auto => 1);
- }
- else {
- $opt{play_all} = 0;
- warn_last_page();
- return;
- }
- }
- }
- else {
- $opt{play_all} = 0;
- $opt{play_backwards} = 0;
- __SUB__->($results);
- }
- }
- else {
- $opt{play_all} = 0;
- $opt{play_backwards} = 0;
- }
- }
- state @keywords;
- if ($args{auto}) { } # do nothing...
- else {
- @keywords = get_input_for_search();
- if (scalar(@keywords) == 0) { # only arguments
- __SUB__->($results);
- }
- }
- state @for_search;
- state @for_play;
- my @copy_of_keywords = @keywords;
- my $contains_keywords = grep /$non_digit_or_opt_re/, @keywords;
- while (@keywords) {
- my $key = shift @keywords;
- if ($key =~ /$valid_opt_re/) {
- my $opt = $1;
- if (
- general_options(opt => $opt,
- res => $videos,)
- ) {
- ## ok
- }
- elsif ($opt =~ /^(?:h|help)\z/) {
- print $complete_help;
- press_enter_to_continue();
- }
- elsif ($opt =~ /^(?:n|next)\z/) {
- if (defined $info->{nextPageToken}) {
- my $request = $yv_obj->next_page($url, $info->{nextPageToken});
- __SUB__->($request, @keywords ? (auto => 1) : ());
- }
- else {
- warn_last_page();
- if ($opt{auto_next_page}) {
- $opt{auto_next_page} = 0;
- @copy_of_keywords = ();
- last;
- }
- }
- }
- elsif ($opt =~ /^(?:b|back|p|prev|previous)\z/) {
- if (defined $info->{prevPageToken}) {
- __SUB__->($yv_obj->previous_page($url, $info->{prevPageToken}), @keywords ? (auto => 1) : ());
- }
- else {
- warn_first_page();
- }
- }
- elsif ($opt =~ /^(?:R|refresh)\z/) {
- @{$videos} = @{$yv_obj->_get_results($url)->{results}{items}};
- }
- elsif ($opt =~ /^(?:r|return)\z/) {
- return;
- }
- elsif ($opt =~ /^(?:a(?:uthor)?|u)${digit_or_equal_re}(.*)/) {
- if (my @nums = get_valid_numbers($#{$videos}, $1)) {
- foreach my $id (@nums) {
- my $channel_id = $yv_utils->get_channel_id($videos->[$id]);
- my $request = $yv_obj->uploads($channel_id);
- if ($yv_utils->has_entries($request)) {
- __SUB__->($request);
- }
- else {
- warn_no_results('video');
- }
- }
- }
- else {
- warn_no_thing_selected('video');
- }
- }
- elsif ($opt =~ /^(?:ps|s2p)${digit_or_equal_re}(.*)/) {
- if (my @nums = get_valid_numbers($#{$videos}, $1)) {
- select_and_save_to_playlist(map { $yv_utils->get_video_id($videos->[$_]) } @nums);
- }
- else {
- warn_no_thing_selected('video');
- }
- }
- elsif ($opt =~ /^(?:p(?:laylists?)?|up)${digit_or_equal_re}(.*)/) {
- if (my @nums = get_valid_numbers($#{$videos}, $1)) {
- foreach my $id (@nums) {
- my $request = $yv_obj->playlists($yv_utils->get_channel_id($videos->[$id]));
- if ($yv_utils->has_entries($request)) {
- print_playlists($request);
- }
- else {
- warn_no_results('playlist');
- }
- }
- }
- else {
- warn_no_thing_selected('video');
- }
- }
- elsif ($opt =~ /^((?:dis)?like)${digit_or_equal_re}(.*)/) {
- my $rating = $1;
- if (my @nums = get_valid_numbers($#{$videos}, $2)) {
- rate_videos($rating, map { $yv_utils->get_video_id($videos->[$_]) } @nums);
- }
- else {
- warn_no_thing_selected('video');
- }
- }
- elsif ($opt =~ /^(?:fav(?:orite)?|F)${digit_or_equal_re}(.*)/) {
- if (my @nums = get_valid_numbers($#{$videos}, $1)) {
- favorite_videos(map { $yv_utils->get_video_id($videos->[$_]) } @nums);
- }
- else {
- warn_no_thing_selected('video');
- }
- }
- elsif ($opt =~ /^(?:subscribe|S)${digit_or_equal_re}(.*)/) {
- if (my @nums = get_valid_numbers($#{$videos}, $1)) {
- subscribe_to_channels(map { $yv_utils->get_channel_id($videos->[$_]) } @nums);
- }
- else {
- warn_no_thing_selected('video');
- }
- }
- elsif ($opt =~ /^(?:en)?q(?:ueue)?+${digit_or_equal_re}(.*)/) {
- if (my @nums = get_valid_numbers($#{$videos}, $1)) {
- push @{$opt{_queue_play}}, map { $yv_utils->get_video_id($videos->[$_]) } @nums;
- }
- else {
- warn_no_thing_selected('video');
- }
- }
- elsif ($opt =~ /^(?:pq|qp|play-queue)\z/) {
- if (ref $opt{_queue_play} eq 'ARRAY' and @{$opt{_queue_play}}) {
- my $ids = 'v=' . join(q{,}, splice @{$opt{_queue_play}});
- general_options(opt => $ids);
- }
- else {
- warn colored("\n[!] The playlist is empty!", 'bold red') . "\n";
- }
- }
- elsif ($opt =~ /^c(?:omments?)?${digit_or_equal_re}(.*)/) {
- if (my @nums = get_valid_numbers($#{$videos}, $1)) {
- get_and_print_comments(map { $yv_utils->get_video_id($videos->[$_]) } @nums);
- }
- else {
- warn_no_thing_selected('video');
- }
- }
- elsif ($opt =~ /^r(?:elated)?${digit_or_equal_re}(.*)/) {
- if (my ($id) = get_valid_numbers($#{$videos}, $1)) {
- get_and_print_related_videos($yv_utils->get_video_id($videos->[$id]));
- }
- else {
- warn_no_thing_selected('video');
- }
- }
- elsif ($opt =~ /^d(?:ownload)?${digit_or_equal_re}(.*)/) {
- if (my @nums = get_valid_numbers($#{$videos}, $1)) {
- local $opt{download_video} = 1;
- play_videos([@{$videos}[@nums]]);
- }
- else {
- warn_no_thing_selected('video');
- }
- }
- elsif ($opt =~ /^(?:play|P)${digit_or_equal_re}(.*)/) {
- if (my @nums = get_valid_numbers($#{$videos}, $1)) {
- local $opt{download_video} = 0;
- local $opt{extract_info} = undef;
- play_videos([@{$videos}[@nums]]);
- }
- else {
- warn_no_thing_selected('video');
- }
- }
- elsif ($opt =~ /^i(?:nfo)?${digit_or_equal_re}(.*)/) {
- if (my @nums = get_valid_numbers($#{$videos}, $1)) {
- foreach my $num (@nums) {
- print_video_info($videos->[$num]);
- }
- press_enter_to_continue();
- }
- else {
- warn_no_thing_selected('video');
- }
- }
- elsif ($opt eq 'anp') { # auto-next-page
- $opt{auto_next_page} = 1;
- }
- elsif ($opt eq 'nnp') { # no-next-page
- $opt{auto_next_page} = 0;
- }
- elsif ($opt =~ /^[ks]re(?:gex)?=(.*)/) {
- my $value = $1;
- if ($value =~ /^([a-zA-Z]++)(?>,|=>)(.+)/) {
- play_videos_matched_by_regex(
- key => $1,
- regex => $2,
- videos => $videos,
- )
- or __SUB__->($results);
- }
- else {
- warn_invalid("Special Regexp", $value);
- }
- }
- elsif ($opt =~ /^re(?:gex)?=(.*)/) {
- play_videos_matched_by_regex(
- key => 'title',
- regex => $1,
- videos => $videos,
- )
- or __SUB__->($results);
- }
- else {
- warn_invalid('option', $opt);
- }
- }
- elsif (youtube_urls($key)) {
- ## ok
- }
- elsif (!$contains_keywords and (valid_num($key, $videos) or $key =~ /$range_num_re/)) {
- my @for_play;
- if ($key =~ /$range_num_re/) {
- my $from = $1;
- my $to = $2 // do {
- $opt{auto_next_page} ? do { $from = 1 } : do { $opt{auto_next_page} = 1 };
- $#{$videos} + 1;
- };
- my @ids = get_valid_numbers($#{$videos}, "$from..$to");
- continue if not @ids;
- push @for_play, @ids;
- }
- else {
- push @for_play, $key - 1;
- }
- if (not play_videos([@{$videos}[@for_play]])) {
- __SUB__->($results);
- }
- if ($opt{autohide_watched}) {
- splice(@{$videos}, $key, 1) for @for_play;
- }
- }
- else {
- push @for_search, $key;
- }
- }
- if (@for_search) {
- __SUB__->($yv_obj->search_videos([splice(@for_search)]));
- }
- elsif ($opt{auto_next_page}) {
- @keywords = (':next', grep { $_ !~ /^:(n|next|anp)\z/ } @copy_of_keywords);
- if (@keywords > 1) {
- my $timeout = 2;
- print colored("\n[*] Press <ENTER> in $timeout seconds to stop the :anp option.", 'bold green');
- eval {
- local $SIG{ALRM} = sub {
- die "alarm\n";
- };
- alarm $timeout;
- scalar <STDIN>;
- alarm 0;
- };
- if ($@) {
- if ($@ eq "alarm\n") {
- __SUB__->($results, auto => 1);
- }
- else {
- warn colored("\n[!] Unexpected error: <$@>.", 'bold red') . "\n";
- }
- }
- else {
- $opt{auto_next_page} = 0;
- __SUB__->($results);
- }
- }
- else {
- warn colored("\n[!] Option ':anp' works only combined with other options!", 'bold red') . "\n";
- $opt{auto_next_page} = 0;
- __SUB__->($results);
- }
- }
- __SUB__->($results) if not $args{auto};
- return 1;
- }
- sub press_enter_to_continue {
- scalar $term->readline(colored("\n=>> Press ENTER to continue...", 'bold'));
- }
- sub main_quit {
- exit($_[0] // 0);
- }
- main_quit(0);
Add Comment
Please, Sign In to add comment