diff options
Diffstat (limited to 'bin/straw-viewer')
-rwxr-xr-x | bin/straw-viewer | 4183 |
1 files changed, 0 insertions, 4183 deletions
diff --git a/bin/straw-viewer b/bin/straw-viewer deleted file mode 100755 index 6fa34ef..0000000 --- a/bin/straw-viewer +++ /dev/null @@ -1,4183 +0,0 @@ -#!/usr/bin/perl - -# Copyright (C) 2010-2020 Trizen <echo dHJpemVuQHByb3Rvbm1haWwuY29tCg== | base64 -d>. -# -# 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 https://dev.perl.org/licenses/ for more information. -# -#------------------------------------------------------- -# straw-viewer -# Fork: 14 February 2020 -# Edit: 14 February 2020 -# https://github.com/trizen/straw-viewer -#------------------------------------------------------- - -# straw-viewer is a command line utility for streaming YouTube videos in mpv/vlc/mplayer. - -# This is a fork of youtube-viewer: -# https://github.com/trizen/youtube-viewer - -=head1 NAME - -straw-viewer - YouTube from command line. - -See: straw-viewer --help - straw-viewer --tricks - straw-viewer --examples - straw-viewer --stdin-help - -=head1 LICENSE AND COPYRIGHT - -Copyright 2010-2020 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 = 1), lib => qw(../lib); # devel mode - -use WWW::StrawViewer v0.0.1; -use WWW::StrawViewer::RegularExpressions; - -use File::Spec::Functions qw( - catdir - catfile - curdir - path - rel2abs - tmpdir - file_name_is_absolute - ); - -binmode(STDOUT, ':utf8'); - -my $appname = 'CLI Straw Viewer'; -my $version = $WWW::StrawViewer::VERSION; -my $execname = 'straw-viewer'; - -# A better <STDIN> support: -require Term::ReadLine; -my $term = Term::ReadLine->new("$appname $version"); - -# 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 = $ENV{HOME} || 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, 'cli-history.txt'); -my $watched_file = catfile($config_dir, 'watched.txt'); -my $api_file = catfile($config_dir, 'api.json'); - -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}) { - my $cmd_path = catfile($path, $cmd); - if (-f -x $cmd_path) { - 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 - ), - - # YouTube options - dash_support => 1, - dash_mp4_audio => 1, - dash_segmented => 1, # may load slow - maxResults => 20, - resolution => 'best', - videoDefinition => undef, - videoDimension => undef, - videoLicense => undef, - safeSearch => undef, - videoCaption => undef, - videoDuration => undef, - videoSyndicated => undef, - publishedBefore => undef, - publishedAfter => undef, - order => undef, - - comments_order => 'top', # valid values: top, new - subscriptions_order => 'relevance', # valid values: alphabetical, relevance, unread - - hl => 'en_US', - regionCode => undef, - - # URI options - youtube_video_url => 'https://www.youtube.com/watch?v=%s', - - # Subtitle options - srt_languages => ['en', 'es'], - get_captions => 1, - auto_captions => 0, - copy_caption => 0, - captions_dir => catdir(tmpdir(), 'straw-viewer'), - cache_dir => catdir(tmpdir(), 'straw-viewer'), - - # API - api_host => "https://invidio.us", - - # Others - autoplay_mode => 0, - http_proxy => undef, - env_proxy => 1, - confirm => 0, - debug => 0, - page => 1, - colors => $constant{win32} ^ 1, - skip_if_exists => 1, - prefer_mp4 => 0, - prefer_av1 => 0, - fat32safe => $constant{win32}, - fullscreen => 0, - results_with_details => 0, - results_with_colors => 0, - results_fixed_width => undef, # auto-defined - show_video_info => 1, - interactive => 1, - get_term_width => $constant{win32} ^ 1, - download_with_wget => undef, # auto-defined - thousand_separator => q{,}, - downloads_dir => curdir(), - keep_original_video => 0, - download_and_play => 0, - autohide_watched => 0, - skip_watched => 0, - remember_watched => 0, - watched_file => $watched_file, - highlight_watched => 1, - highlight_color => 'bold', - remove_played_file => 0, - history => undef, # auto-defined - history_limit => 100_000, - history_file => $history_file, - convert_cmd => 'ffmpeg -i *IN* *OUT*', - convert_to => undef, - - custom_layout => undef, # auto-defined - custom_layout_format => [{width => 3, align => "right", color => "bold", text => "*NO*.",}, - {width => "55%", align => "left", color => "bold blue", text => "*TITLE*",}, - {width => "15%", align => "left", color => "yellow", text => "*AUTHOR*",}, - {width => 3, align => "right", color => "green", text => "*AGE_SHORT*",}, - {width => 5, align => "right", color => "green", text => "*VIEWS_SHORT*",}, - {width => 8, align => "right", color => "blue", text => "*TIME*",}, - ], - - 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 page of results -: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 - -sv funny cats : search for videos - -sc mathematics : search for channels - -sp classical music : search for playlists -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 -:u(ploads)=i : display author's latest uploads -:pv=i :popular=i : display author's popular uploads -:A(ctivity)=i : display author's recent activity -: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 -:autoplay=i : autoplay mode, starting from video i - -# 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 { - my ($config_file) = @_; - - 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"; - - if ($home_dir eq $ENV{HOME}) { - $dumped_config =~ s/\Q$home_dir\E/\$ENV{HOME}/g; - } - - print $config_fh $config_documentation, $dumped_config; - close $config_fh; - } -} - -our $CONFIG; - -sub load_config { - my ($config_file) = @_; - - if (not -e $config_file or -z _ or $opt{reconfigure}) { - dump_configuration($config_file); - } - - 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!"; - } - - # 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 $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; - } - } - - if (not $CONFIG{video_player_selected}) { - warn "\n[!] Please install a supported video player! (e.g.: mpv)\n\n"; - $CONFIG{video_player_selected} = 'mpv'; - } - } - elsif ($CONFIG{video_player_selected} =~ /mpv/i) { # update for mpv 0.32 (#290) -#<<< - my $mpv = $CONFIG{video_players}{$CONFIG{video_player_selected}}; - if ( - ($mpv->{arg} =~ s/(--title)\s+(\*TITLE\*)/$1=$2/g) - | ($mpv->{audio} =~ s/(--audio-file)\s+(\*AUDIO\*)/$1=$2/g) - | ($mpv->{srt} =~ s/(--sub-file)\s+(\*SUB\*)/$1=$2/g) - ) { - say ":: Updated configuration to support mpv 0.32"; - $update_config = 1; - } -#>>> - } - - # Download with wget if it is installed - if (not defined $CONFIG{download_with_wget}) { - - my $wget_path = which_command('wget'); - - if (defined($wget_path)) { - $CONFIG{wget_cmd} = $wget_path; - $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}) { - - my $ffmpeg_path = which_command('ffmpeg'); - - if (defined($ffmpeg_path)) { - $CONFIG{ffmpeg_cmd} = $ffmpeg_path; - $CONFIG{merge_into_mkv} = 1; - } - else { - $CONFIG{merge_into_mkv} = 0; - } - - $update_config = 1; - } - - # Fixed-width format and custom layout - if ( not defined($CONFIG{results_fixed_width}) - or not defined($CONFIG{custom_layout})) { - - if ( eval { require Unicode::GCString; 1 } - or eval { require Text::CharWidth; 1 }) { - $CONFIG{custom_layout} //= 1; - $CONFIG{results_fixed_width} //= 1; - } - else { - $CONFIG{custom_layout} = 0; - $CONFIG{results_fixed_width} = 0; - } - - $update_config = 1; - } - - # Enable history if Term::ReadLine::Gnu::XS is installed - if (not defined $CONFIG{history}) { - - if ($term->can('ReadHistory')) { - $CONFIG{history} = 1; - } - else { - $CONFIG{history} = 0; - } - - $update_config = 1; - } - - foreach my $key (keys %CONFIG) { - if (not exists $CONFIG->{$key}) { - $update_config = 1; - last; - } - } - - dump_configuration($config_file) if $update_config; - -foreach my $path($CONFIG{cache_dir}, $CONFIG{captions_dir}) { - next if -d $path; - require File::Path; - File::Path::make_path($path) - or warn "[!] Can't create path <<$path>>: $!"; -} - - @opt{keys %CONFIG} = values(%CONFIG); -} - -load_config($config_file); - -if ($opt{remember_watched}) { - if (-f $opt{watched_file}) { - if (open my $fh, '<', $opt{watched_file}) { - chomp(my @ids = <$fh>); - @watched_videos{@ids} = (); - close $fh; - } - else { - warn "[!] Can't open the watched file `$opt{watched_file}' for reading: $!"; - } - } -} - -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 @history = $term->history_list; - - # Rewrite the history file, when the history_limit has been reached. - if ($opt{history_limit} > 0 and @history > $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}) { - - # Keep only the most recent half part of the history file - say {$fh} join("\n", @history[($opt{history_limit} >> 1) .. $#history]); - close $fh; - } - } -} - -my $yv_obj = WWW::StrawViewer->new( - escape_utf8 => 1, - config_dir => $config_dir, - cache_dir => $opt{cache_dir}, - lwp_env_proxy => $opt{env_proxy}, - authentication_file => $authentication_file, - ); - -require WWW::StrawViewer::Utils; -my $yv_utils = WWW::StrawViewer::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 --uploads=s : list videos uploaded by a specific channel or user - -pv --popular=s : list the most popular videos from a specific channel - -uf --favorites=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 - -sv --search-videos : search for YouTube videos (default mode) - - * Playlists - -up --playlists=s : list playlists created by a specific channel or user - -sp --search-pl : search for playlists of videos - --pid=s : list a playlist of videos by playlist ID - --pp=s,s : play the videos from the given playlist IDs - --ps=s : add video by ID or URL to a post-selected playlist - or in a given playlist ID specified with `--pid` - --position=i : position in the playlist where to add the video - -* Activities - -ua --activities:s : show activity events for a given channel - -* Trending - --trending:s : show trending videos in a given category ID or name - use the `--within=s` option to restrict the results - - * Channels - -sc --channels : search for YouTube channels - -* Comments - --comments=s : display comments for a video by ID or URL - --comments-order=s : change the order of YouTube comments - valid values: relevance, time - - * Filtering - --author=s : search in videos uploaded by a specific user - --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=s : search only for videos in a specific category name/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, Nw, 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 number - --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: best, 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 - -U --uploads:s : show the uploads from your channel * - -P --playlists:s : show the playlists from your channel * - -F --favorites:s : show the latest favorited videos * - -S --subscriptions:s : show the subscribed channels * - -SV --subs-videos:s : show the subscription videos (slow) * - --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 given channel ID or username * - --favorite=s : favorite a 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! : play videos in fullscreen mode - -n --audio! : play audio only, without displaying video - --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) - --skip-if-exists! : don't download videos which already exist (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 in 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 the video results in order - -B --backwards! : play the video results in reverse order - -s --shuffle! : shuffle the results of videos - -I --interactive! : interactive mode, prompting for user input - --autoplay! : autoplay mode, automatically playing related videos - --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 - --get-term-width! : allow $execname to read your terminal width - --autohide! : automatically hide watched videos - --skip-watched! : don't play already 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 - --prefer-av1! : prefer videos in AV1 format, instead of WEBM - - * Closed-captions - --get-captions! : download closed-captions for videos - --auto-captions! : include or exclude auto-generated captions - --captions-dir=s : directory where to save the .srt files - - * Config - --config=s : configuration file - --update-config! : update the configuration file - - * Output - -C --colorful! : use colors to delimit the video results - -D --details! : display the results with extra 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 information for a video ID or URL - -e --extract=s : extract information from videos (see: -T) - --extract-file=s : extract 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 - --video-info! : show video information before playing - --escape-info! : quotemeta() the fields of the `--extract` - --use-colors! : enable or disable the ANSI colors for text - - * Other - --invidious! : use the API of invidio.us to get the streaming URLs - --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 - --dash-segmented! : include or exclude segmented DASH streams - - -Help options: - -T --tricks : show more 'hidden' features of $execname - -E --examples : show several usage examples of $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..3 : 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"; - - == straw-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 - > By using the string "mine" where a channel ID is required, "mine" will be - automatically replaced with your channel ID. (requires authentication) - - Examples: - $execname --uploads=mine - $execname --likes=mine - $execname --favorites=mine - $execname --playlists=mine - --> More STDIN help: - > ":r", ":return" will return to the previous page of results. - 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) 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. - - 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. - - Example: - :re=video\\ title == :re="video title" - - > ":anp" stands for "Auto Next Page". How do we use it? - Well, let's search for some videos. Now, if we want to play - only the videos matched by a regex, we'd say :re="REGEX". - But, what if we 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 - *VIEWS_SHORT* : the number of views in abbreviated notation - *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 - *PUBLISHED* : the publication date as "DD MMM YYYY" - *AGE* : the age of a video (N days, N months, N years) - *AGE_SHORT* : the abbreviated age of a video (Nd, Nm, Ny) - *DIMENSION* : the dimension of the video (2D or 3D) - *DEFINITION* : the definition of the video (HD or SD) - *TIME* : the duration of the video as "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 captions - *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 russian music -category=10 -Results: play all the video results (-A) - only audio, no video (-n) - search for "russian music" - in category "10", 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: the most recent 5 videos by a specific author (-up), printed with extra details (-D). - -Command: $execname --author=MIT atom -Results: search only in videos by a specific author. - -Command: $execname --author=MIT atom --within=2y -Results: search only in videos by a specific author, published in the last 2 years. - -Command: $execname --popular=MIT --within=6m -Results: show the most popular videos by a specific user, published in the last 6 months. - -Command: $execname -S=vsauce -Results: get the 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 math lessons -Results: search for YouTube 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: matches title: "Linux video" - -Command: :regex="linux.*part \\d+/\\d+" -Example: matches 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 "$appname $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( - api_host - videoCaption maxResults order - videoDefinition videoCategoryId - videoDimension videoDuration - videoEmbeddable videoLicense - videoSyndicated channelId - publishedAfter publishedBefore - safeSearch regionCode debug hl - http_proxy page comments_order - subscriptions_order - ) - ) { - - if (defined $opt->{$option_name}) { - my $code = \&{"WWW::StrawViewer::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->{category_id}) { - - my $str = delete $opt->{category_id}; - my $category = extract_category($str); - - if (ref($category) eq 'HASH') { - say ":: Category selected: $category->{snippet}{title}" if $yv_obj->get_debug; - $yv_obj->set_videoCategoryId($category->{id}); - } - else { - warn_invalid('category', $str); - } - } - - if (defined $opt->{prefer_mp4}) { - $yv_obj->set_prefer_mp4(delete($opt->{prefer_mp4}) ? 1 : 0); - } - - if (defined $opt->{prefer_av1}) { - $yv_obj->set_prefer_av1(delete($opt->{prefer_av1}) ? 1 : 0); - } - - if (defined $opt->{hd}) { - $yv_obj->set_videoDefinition(delete($opt->{hd}) ? 'high' : 'any'); - } - - if (defined $opt->{author}) { - my $name = delete $opt->{author}; - if (my $id = extract_channel_id($name)) { - - if (not $yv_utils->is_channelID($id)) { - $id = $yv_obj->channel_id_from_username($id) // do { - warn_invalid("username or channel ID", $id); - undef; - }; - } - - if ($id eq 'mine') { - $id = $yv_obj->my_channel_id() // do { - warn_invalid("username or channel ID", $id); - undef; - }; - } - - $yv_obj->set_channelId($id); - } - else { - warn_invalid("username or channel ID", $name); - } - } - - if (defined $opt->{within}) { - my $value = delete $opt->{within}; - - if ($value =~ /^\s*(\d+(?:\.\d+)?)([dwmy])/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; - no warnings 'redefine'; - *colored = \&Term::ANSIColor::colored; - *colorstrip = \&Term::ANSIColor::colorstrip; - } - else { - no warnings 'redefine'; - *colored = sub { $_[0] }; - *colorstrip = sub { $_[0] }; - } - } - - # ... SUBROUTINE CALLS ... # - if (defined $opt->{subscribe}) { - subscribe(split(/[,\s]+/, delete $opt->{subscribe})); - } - - 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 (delete $opt->{search_videos}) { - print_videos($yv_obj->search_videos([@{$keywords}])); - } - - if (delete $opt->{search_channels}) { - print_channels($yv_obj->search_channels([@{$keywords}])); - } - - if (delete $opt->{search_playlists}) { - print_playlists($yv_obj->search_playlists([@{$keywords}])); - } - - if (delete $opt->{categories}) { - print_categories($yv_obj->video_categories); - } - - if (defined $opt->{uploads}) { - my $str = delete $opt->{uploads}; - - if ($str) { - if (my $id = extract_channel_id($str)) { - $yv_utils->is_channelID($id) - ? print_videos($yv_obj->uploads($id)) - : print_videos($yv_obj->uploads_from_username($id)); - } - else { - warn_invalid("username or channel ID", $str); - } - } - else { - print_videos($yv_obj->uploads); - } - } - - if (defined $opt->{popular_videos}) { - my $str = delete $opt->{popular_videos}; - - if (my $id = extract_channel_id($str)) { - - if (not $yv_utils->is_channelID($id)) { - $id = $yv_obj->channel_id_from_username($id) // do { - warn_invalid("username or channel ID", $id); - undef; - }; - } - - if ($id eq 'mine') { - $id = $yv_obj->my_channel_id() // do { - warn_invalid("username or channel ID", $id); - undef; - }; - } - - print_videos($yv_obj->popular_videos($id)); - } - else { - warn_invalid("username or channel ID", $str); - } - } - - if (defined $opt->{trending}) { - - my $str = delete $opt->{trending}; - my $category = extract_category($str); - my $cat_id = undef; - - if (ref($category) eq 'HASH') { - say ":: Category selected: $category->{snippet}{title}" if $yv_obj->get_debug; - $cat_id = $category->{id}; - } - elsif ($str) { - warn_invalid('category', $str); - } - - print_videos($yv_obj->trending_videos_from_category($cat_id)); - } - - if (defined $opt->{subscriptions}) { - my $str = delete $opt->{subscriptions}; - - if ($str) { - if (my $id = extract_channel_id($str)) { - $yv_utils->is_channelID($id) - ? print_channels($yv_obj->subscriptions($id)) - : print_channels($yv_obj->subscriptions_from_username($id)); - } - else { - warn_invalid("username or channel ID", $str); - } - } - else { - print_channels($yv_obj->subscriptions); - } - } - - if (defined $opt->{subscription_videos}) { - my $str = delete $opt->{subscription_videos}; - - if ($str) { - if (my $id = extract_channel_id($str)) { - $yv_utils->is_channelID($id) - ? print_videos($yv_obj->subscription_videos($id)) - : print_videos($yv_obj->subscription_videos_from_username($id)); - } - else { - warn_invalid("username or channel ID", $str); - } - } - else { - print_videos($yv_obj->subscription_videos); - } - } - - if (defined $opt->{related_videos}) { - get_and_print_related_videos(split(/[,\s]+/, delete($opt->{related_videos}))); - } - - if (defined $opt->{playlists}) { - my $str = delete($opt->{playlists}); - - if ($str) { - if (my $id = extract_channel_id($str)) { - $yv_utils->is_channelID($id) - ? print_playlists($yv_obj->playlists($id)) - : print_playlists($yv_obj->playlists_from_username($id)); - } - else { - warn_invalid("username or channel ID", $str); - warn colored("[+] To search for playlists, try: $0 -sp $str", 'bold yellow') . "\n"; - } - } - else { - print_playlists($yv_obj->my_playlists); - } - } - - if (defined $opt->{favorites}) { - my $str = delete($opt->{favorites}); - - if ($str) { - if (my $id = extract_channel_id($str)) { - $yv_utils->is_channelID($id) - ? print_videos($yv_obj->favorites($id)) - : print_videos($yv_obj->favorites_from_username($id)); - } - else { - warn_invalid("username or channel ID", $str); - } - } - else { - print_videos($yv_obj->favorites); - } - } - - if (defined $opt->{likes}) { - my $str = delete($opt->{likes}); - - if ($str) { - if (my $id = extract_channel_id($str)) { - $yv_utils->is_channelID($id) - ? print_videos($yv_obj->likes($id)) - : print_videos($yv_obj->likes_from_username($id)); - } - else { - warn_invalid("username or channel ID", $str); - } - } - else { - print_videos($yv_obj->my_likes); - } - } - - if (defined $opt->{dislikes}) { - delete $opt->{dislikes}; - print_videos($yv_obj->my_dislikes); - } - - if (defined $opt->{activities}) { - my $str = delete $opt->{activities}; - - if ($str) { - if (my $id = extract_channel_id($str)) { - $yv_utils->is_channelID($id) - ? print_videos($yv_obj->activities($id)) - : print_videos($yv_obj->activities_from_username($id)); - } - else { - warn_invalid("username or channel ID", $str); - } - } - else { - print_videos($yv_obj->my_activities); - } - } - - 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'); - }; - - my %orig_opt = %opt; - my $orig_config_file = "$config_file"; - - 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, - - 'config=s' => \$config_file, - 'update-config!' => sub { dump_configuration($config_file) }, - - # 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}, - 'comments-order=s' => \$opt{comments_order}, - - 'c|categories' => \$opt{categories}, - 'video-ids|videoids|id|ids=s' => \$opt{play_video_ids}, - - 'search-videos|search|sv!' => \$opt{search_videos}, - 'search-channels|channels|sc!' => \$opt{search_channels}, - 'search-playlists|sp|p!' => \$opt{search_playlists}, - - 'subscriptions|S:s' => \$opt{subscriptions}, - 'subs-videos|SV:s' => \$opt{subscription_videos}, - 'subs-order=s' => \$opt{subscriptions_order}, - 'uploads|U|user|user-videos|uv|u:s' => \$opt{uploads}, - 'favorites|F|user-favorites|uf:s' => \$opt{favorites}, - 'playlists|P|user-playlists|up:s' => \$opt{playlists}, - 'activities|activity|ua:s' => \$opt{activities}, - 'likes|L|user-likes|ul:s' => \$opt{likes}, - 'dislikes' => \$opt{dislikes}, - 'subscribe=s' => \$opt{subscribe}, - - 'trending|trends:s' => \$opt{trending}, - 'playlist-id|pid=s' => \$opt{playlist_id}, - - # English-UK friendly - 'favorite|favourite|favorite-video|favourite-video|fav=s' => \$opt{favorite_video}, - - 'login|authenticate' => \$opt{authenticate}, - 'logout' => \$opt{logout}, - - 'related-videos|rv=s' => \$opt{related_videos}, - 'popular-videos|popular|pv=s' => \$opt{popular_videos}, - - 'http_proxy|http-proxy|proxy=s' => \$opt{http_proxy}, - - 'catlang|cl|hl=s' => \$opt{hl}, - 'category|cat-id|cat=s' => \$opt{category_id}, - '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}, - '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}, - - 'autoplay!' => \$opt{autoplay_mode}, - - '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-arg|append-args=s' => \$MPLAYER{user_defined_arguments}, - - # 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}, - 'prefer-av1!' => \$opt{prefer_av1}, - - '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}, - - 'invidious!' => \$opt{use_invidious_api}, - 'convert-command|convert-cmd=s' => \$opt{convert_cmd}, - 'dash-m4a|dash-mp4-audio|dash-mp4a!' => \$opt{dash_mp4_audio}, - 'dash-segmented!' => \$opt{dash_segmented}, - 'wget-dl|wget-download!' => \$opt{download_with_wget}, - 'filename|filename-format=s' => \$opt{video_filename_format}, - 'rp|rem-played|remove-played-file!' => \$opt{remove_played_file}, - 'info|i|video-info=s' => \$opt{print_video_info}, - 'get-term-width!' => \$opt{get_term_width}, - 'page=i' => \$opt{page}, - 'novideo|no-video|n|audio!' => \$opt{novideo}, - 'autohide!' => \$opt{autohide_watched}, - 'highlight!' => \$opt{highlight_watched}, - 'skip-watched!' => \$opt{skip_watched}, - 'results=i' => \$opt{maxResults}, - 'shuffle|s!' => \$opt{shuffle}, - 'more|m!' => \$opt{more_results}, - 'pos|position=i' => \$opt{position}, - 'ps|playlist-save=s' => \$opt{playlist_save}, - - 'quiet|q!' => \$opt{quiet}, - 'really-quiet!' => \$opt{really_quiet}, - 'video-info!' => \$opt{show_video_info}, - - '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"; - - if ($config_file ne $orig_config_file) { # load the config file specified with `--config=s` - ##say ":: Loading config: $config_file"; - $config_file = rel2abs($config_file); - - my %new_opt = %opt; - load_config($config_file); - - foreach my $key (keys %new_opt) { - if ( defined($new_opt{$key}) - and defined($orig_opt{$key}) - and $new_opt{$key} ne $orig_opt{$key}) { - $opt{$key} = $new_opt{$key}; - } - } - } - - 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; - - if (not 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; - - if (not defined $id) { - warn_invalid('playlistID', $value); - return; - } - - return $id; -} - -sub extract_category { - my ($str) = @_; - - $str || return; - - state $results = $yv_obj->video_categories; - return if ref($results) ne 'HASH'; - - my $categories = $results->{items}; - return if ref($categories) ne 'ARRAY'; - - foreach my $category (@$categories) { - if ($category->{id} eq $str) { - return $category; - } - } - - my $str_re = qr/\Q$str\E/i; - - foreach my $category (@$categories) { - if ($yv_utils->get_title($category) =~ /$str_re/) { - return $category; - } - } - - return; -} - -sub extract_channel_id { - my ($str) = @_; - - if ($str =~ /$get_channel_videos_id_re/) { - return $+{channel_id}; - } - - if ($str =~ /$get_username_videos_re/) { - return $+{username}; - } - - if ($str =~ /$valid_channel_id_re/) { - return $+{channel_id}; - } - - if ($str =~ /^[-a-zA-Z0-9_]+\z/) { - return $str; - } - - return undef; -} - -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(@_); -} - -sub clear_title { - my ($title) = @_; - - $title =~ s/[^\w\s[:punct:]]//g; - $title = join(' ', split(' ', $title)); - - return $title; -} - -# 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}) { - my $str = join(' ', grep { /\w/ } @{$args}, @{$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() // return; - 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; - my $pos = $opt{position}; # position in the playlist - - if (!defined($pos) or $pos < 0) { - local $yv_obj->{maxResults} = 1; - - my $info = $yv_obj->videos_from_playlist_id($playlistID); - my $total_results = $info->{results}{pageInfo}{totalResults}; - - say "\n:: Total number of videos in the playlist: $total_results"; - $pos //= 0; - $pos += $total_results || 0; - say ":: Saving video at position: $pos"; - } - - if ($yv_obj->add_video_to_playlist($playlistID, $videoID, $pos)) { - printf(":: 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)) { - printf("\n:: Video %s has been successfully %sd.\n", sprintf($CONFIG{youtube_video_url}, $videoID), $rating); - } - else { - warn_cant_do($rating, $videoID); - } - } - - return 1; -} - -sub get_and_play_video_ids { - (my @ids = grep { get_valid_video_id($_) } @_) || return; - my $info = $yv_obj->video_details(join(',', @ids)); - - if ($yv_utils->has_entries($info)) { - if (not play_videos([$info->{results}])) { - return; - } - } - else { - warn_cant_do('get info for', @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); - - if ($yv_utils->has_entries($info)) { - local $opt{show_video_info} = 1; - print_video_info($info->{results}{items}[0]); - } - else { - warn_cant_do('get info for', $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 { - my (@ids) = @_; - - return if not authenticated(); - - foreach my $channel (@ids) { - - my $id = extract_channel_id($channel) // do { - warn_invalid("channel ID or username", $channel); - next; - }; - - my $ok = - $yv_utils->is_channelID($id) - ? $yv_obj->subscribe_channel($id) - : $yv_obj->subscribe_channel_from_username($id); - - if ($ok) { - print ":: Successfully subscribed to channel: $id\n"; - } - else { - warn colored("\n[!] Unable to subscribe to channel: $id", 'bold red') . "\n"; - } - } - - return 1; -} - -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}; - my $token = $args{token}; - - if (not defined($option)) { - return; - } - - if ($option =~ /^(?:q|quit|exit)\z/) { - main_quit(0); - } - elsif ($option =~ /^(?:n|next)\z/ and defined $url) { - if (exists $args{token}) { - if (defined $token) { - my $request = $yv_obj->next_page_with_token($url, $token); - $callback->($request); - } - else { - warn_last_page(); - } - } - else { - my $request = $yv_obj->next_page($url); - $callback->($request); - } - } - elsif ($option =~ /^(?:b|back|p|prev|previous)\z/ and defined $url) { - my $request = $yv_obj->previous_page($url); - $callback->($request); - } - elsif ($option =~ /^(?:R|refresh)\z/ and defined $url) { - @{$results} = @{$yv_obj->_get_results($url)->{results}{items}}; - } - 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, @ids) = @_; - - foreach my $videoID (@ids) { - warn colored("\n[!] Can't $action video: " . sprintf($CONFIG{youtube_video_url}, $videoID), 'bold red') . "\n"; - - my %info = $yv_obj->_get_video_info($videoID); - my $resp = $yv_obj->parse_json_string($info{player_response} // next); - - if (eval { exists($resp->{playabilityStatus}) and $resp->{playabilityStatus}{status} =~ /error/i }) { - warn colored("[+] Reason: $resp->{playabilityStatus}{reason}.", 'bold yellow') . "\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 adjust_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 $channels = $results->{results} // []; - - foreach my $i (0 .. $#{$channels}) { - my $channel = $channels->[$i]; - - if ($opt{results_with_details}) { - printf( - "\n%s. %s\n %s: %-23s %s: %-12s\n%s\n", - colored(sprintf('%2d', $i + 1), 'bold') => colored($yv_utils->get_channel_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_with_colors}) { - print "\n" if $i == 0; - printf("%s. %s (%s)\n", - colored(sprintf('%2d', $i + 1), 'bold'), - colored($yv_utils->get_channel_title($channel), 'blue'), - colored($yv_utils->get_publication_date($channel), 'magenta'), - ); - } - elsif ($opt{results_fixed_width}) { - - require List::Util; - - my @authors = map { $yv_utils->get_channel_id($_) } @{$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]; - my $title = clear_title($yv_utils->get_channel_title($channel)); - - printf "%s. %s %s [%*s]\n", colored(sprintf('%2d', $i + 1), 'bold'), - adjust_width($title, $title_length), - adjust_width($authors[$i], $author_width, 1), - $dates_width, $dates[$i]; - } - last; - } - else { - print "\n" if $i == 0; - printf "%s. %s [%s]\n", colored(sprintf('%2d', $i + 1), 'bold'), $yv_utils->get_title($channel), - $yv_utils->get_publication_date($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 => $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, $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 $comments = $results->{results}{comments} // []; - my $token = $results->{results}{continuation}; - - my $i = 0; - foreach my $comment (@{$comments}) { - my $comment_id = $yv_utils->get_comment_id($comment); - my $comment_age = $yv_utils->get_publication_age_approx($comment); - - printf( - "\n%s (%s) commented:\n%s\n", - colored($yv_utils->get_author($comment), 'bold'), - ( - $comment_age =~ /sec|min|hour|day/ - ? "$comment_age ago" - : $yv_utils->get_publication_date($comment) - ), - wrap_text( - i_tab => q{ } x 3, - s_tab => q{ } x 3, - text => [$yv_utils->get_comment_content($comment) // 'Empty comment...'] - ), - ); - - #~ if (exists $comment->{replies}) { - #~ foreach my $reply (reverse @{$comment->{replies}{comments}}) { - #~ my $reply_age = $yv_utils->date_to_age($reply->{snippet}{publishedAt}); - #~ printf( - #~ "\n %s (%s) replied:\n%s\n", - #~ colored($reply->{snippet}{authorDisplayName}, 'bold'), - #~ ( - #~ $reply_age =~ /sec|min|hour|day/ - #~ ? "$reply_age ago" - #~ : $yv_utils->format_date($reply->{snippet}{publishedAt}) - #~ ), - #~ wrap_text( - #~ i_tab => q{ } x 6, - #~ s_tab => q{ } x 6, - #~ text => [$reply->{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 => $results, - mode => 'comments', - args => [$videoID], - token => $token, - ) - ) { - ## 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) = @_; - - return if ref($results) ne 'HASH'; - my $categories = $results->{items}; - return if ref($categories) ne 'ARRAY'; - - my $i = 0; - print "\n" if @{$categories}; - - # Filter out nonassignable categories - @$categories = grep { $_->{snippet}{assignable} } @$categories; - - foreach my $category (@{$categories}) { - 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 $category = $categories->[$key - 1]; - my $cat_id = $category->{id}; - my $videos = $yv_obj->videos_from_category($cat_id); - - if (not $yv_utils->has_entries($videos)) { - $videos = $yv_obj->trending_videos_from_category($cat_id); - } - - print_videos($videos); - } - 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 $playlists = $results->{results} // []; - - 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_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), 'blue'), - colored($yv_utils->get_publication_date($playlist), 'magenta'), - colored($yv_utils->get_channel_title($playlist), 'green'), - ); - } - 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]; - my $title = clear_title($yv_utils->get_title($playlist)); - - printf "%s. %s %s [%*s]\n", colored(sprintf('%2d', $i + 1), 'bold'), - adjust_width($title, $title_length), - adjust_width($authors[$i], $author_width, 1), - $dates_width, $dates[$i]; - } - last; - } - 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 => $results, - 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::StrawViewer::GetCaption; - my $yv_cap = WWW::StrawViewer::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::StrawViewer::Itags; - state $yv_itags = WWW::StrawViewer::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{novideo} ? 'audio' : $opt{resolution}), - dash => $dash, - dash_mp4_audio => ($opt{novideo} ? 1 : $opt{dash_mp4_audio}), - dash_segmented => ($opt{download_video} ? 0 : $opt{dash_segmented}), - ); - - return { - streaming => $streaming, - srt_file => $srt_file, - info => $info, - resolution => $resolution, - }; -} - -sub download_from_url { - my ($url, $output_filename) = @_; - - # Download with wget - if ($opt{download_with_wget}) { - my @cmd = ($opt{wget_cmd}, '-c', '-t', '10', '--waitretry=3', $url, '-O', "$output_filename.part"); - $yv_obj->proxy_system(@cmd); - return if $?; - rename("$output_filename.part", $output_filename) or return undef; - return $output_filename; - } - - state $lwp_dl = which_command('lwp-download'); - - # Download with lwp-download - if (defined($lwp_dl)) { - my @cmd = ($lwp_dl, $url, "$output_filename.part"); - $yv_obj->proxy_system(@cmd); - return if $?; - rename("$output_filename.part", $output_filename) or return undef; - return $output_filename; - } - - # Download with LWP::UserAgent - 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); - - my $resp = eval { $lwp->mirror($url, "$output_filename.part") }; - - 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 (defined(my $wget_path = which_command('wget'))) { - $CONFIG{wget_cmd} = $wget_path; - $CONFIG{download_with_wget} = 1; - dump_configuration($config_file); - } - else { - warn colored("[!] Please install `wget` and try again...", 'bold red') . "\n\n"; - } - - unlink("$output_filename.part"); - return download_from_url($url, $output_filename); - } - - rename("$output_filename.part", $output_filename) or return undef; - 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 $naked_filename = $video_filename =~ s/\.\w+\z//r; - - $naked_filename =~ s/:\h*/ - /g; # replace colons (":") with dashes ("-") - - my $mkv_filename = "$naked_filename.mkv"; - my $srt_filename = "$naked_filename.srt"; - my $audio_filename = "$naked_filename - audio"; - - my $video_info = $streaming->{streaming}; - my $audio_info = $streaming->{streaming}{__AUDIO__}; - - if ($audio_info) { - $audio_filename .= "." . $yv_utils->extension($audio_info->{type}); - } - - if (not -d $opt{downloads_dir}) { - require File::Path; - if (not 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"; - } - - $mkv_filename = catfile($opt{downloads_dir}, $mkv_filename); - $srt_filename = catfile($opt{downloads_dir}, $srt_filename); - $audio_filename = catfile($opt{downloads_dir}, $audio_filename); - $video_filename = catfile($opt{downloads_dir}, $video_filename); - - if ($opt{skip_if_exists} and -e $mkv_filename) { - $video_filename = $mkv_filename; - say ":: File `$mkv_filename` already exists. Skipping..."; - } - else { - if ($opt{skip_if_exists} and -e $video_filename) { - say ":: File `$video_filename` already exists. Skipping..."; - } - else { - $video_filename = download_from_url($video_info->{url}, $video_filename) // return; - } - - if ($opt{skip_if_exists} and -e $audio_filename) { - say ":: File `$audio_filename` already exists. Skipping..."; - } - elsif ($audio_info) { - $audio_filename = download_from_url($audio_info->{url}, $audio_filename) // return; - } - } - - my @merge_files = ($video_filename); - - if ($audio_info) { - push @merge_files, $audio_filename; - } - - if ( $opt{merge_with_captions} - and defined($streaming->{srt_file}) - and -f $streaming->{srt_file}) { - push @merge_files, $streaming->{srt_file}; - } - - if ( $opt{merge_into_mkv} - and scalar(@merge_files) > 1 - and scalar(grep { -f $_ } @merge_files) == scalar(@merge_files) - and not -e $mkv_filename) { - - say ":: Merging into MKV..."; - - my $ffmpeg_cmd = $opt{ffmpeg_cmd}; - my $ffmpeg_args = $opt{merge_into_mkv_args}; - - if (my @srt_files = grep { /\.srt\z/ } @merge_files) { - my $srt_file = $srt_files[0]; - require File::Basename; - if (File::Basename::basename($srt_file) =~ m{^.{11}_([a-z]{2,4})}i) { - my $lang_code = $1; - $ffmpeg_args .= " -metadata:s:s:0 language=$lang_code"; - } - } - - my $merge_command = - join(' ', $ffmpeg_cmd, (map { "-i \Q$_\E" } @merge_files), $ffmpeg_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 = $srt_filename; - - require File::Copy; - File::Copy::cp($from, $to); - } - - return 1; -} - -sub save_watched_video { - my ($video_id) = @_; - - if ($opt{remember_watched} and not exists($watched_videos{$video_id})) { - $watched_videos{$video_id} = 1; - open my $fh, '>>', $opt{watched_file} or return; - say {$fh} $video_id; - close $fh; - } - - $watched_videos{$video_id} = 1; - 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, - ); - - if ($streaming->{streaming}{url} =~ m{^https://www\.youtube\.com/watch\?v=}) { - $cmd =~ s{ --no-ytdl\b}{ }g; - } - - $has_video ? $cmd : join(' ', $cmd, quotemeta($streaming->{streaming}{url})); -} - -sub autoplay { - my $video_id = get_valid_video_id(shift) // return; - - my %seen = ($video_id => 1); # make sure we don't get stuck in a loop - local $yv_obj->{maxResults} = 10; - - while (1) { - get_and_play_video_ids($video_id) || return; - my $related = $yv_obj->related_to_videoID($video_id); - (my @video_ids = grep { !$seen{$_}++ } map { $yv_utils->get_video_id($_) } @{$related->{results}{items}}) || return; - $video_id = $opt{shuffle} ? $video_ids[rand @video_ids] : $video_ids[0]; - } - - return 1; -} - -sub play_videos { - my ($videos) = @_; - - foreach my $video (@{$videos}) { - - my $video_id = $yv_utils->get_video_id($video); - - if ($opt{autoplay_mode}) { - local $opt{autoplay_mode} = 0; - autoplay($video_id); - next; - } - - # Ignore already watched videos - if (exists($watched_videos{$video_id}) and $opt{skip_watched}) { - say ":: Already watched video (ID: $video_id)... Skipping..."; - next; - } - - 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}, - ); - } - 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; - } - } - - save_watched_video($video_id); - press_enter_to_continue() if $opt{confirm}; - } - - 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::StrawViewer::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) = @_; - - $opt{show_video_info} || return 1; - - my $hr = '-' x ($opt{get_term_width} ? get_term_width() : $term_width); - - printf( - "\n%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)], - ['VideoID' => $yv_utils->get_video_id($video)], - ['Category' => $yv_utils->get_category_name($video)], - ['Definition' => $yv_utils->get_definition($video)], - ['Duration' => $yv_utils->get_time($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) = @_; - - # use Data::Dump qw(pp); - # pp $results; - - 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 $videos = $results->{results} // []; - - if (ref($videos) eq 'HASH' and exists $videos->{videos}) { - $videos = $videos->{videos}; - } - - #my $videos = $info->{items} // []; - - #~ foreach my $entry (@$videos) { - #~ if ($yv_utils->is_activity($entry)) { - #~ my $type = $entry->{snippet}{type}; - - #~ if ($type eq 'upload') { - #~ $entry->{kind} = 'youtube#video'; - #~ $entry->{id} = $entry->{contentDetails}{upload}{videoId}; - #~ } - - #~ if ($type eq 'playlistItem') { - #~ $entry->{kind} = 'youtube#video'; - #~ $entry->{id} = $entry->{contentDetails}{playlistItem}{resourceId}{videoId}; - #~ } - - #~ if ($type eq 'bulletin' and $entry->{contentDetails}{bulletin}{resourceId}{kind} eq 'youtube#video') { - #~ $entry->{kind} = 'youtube#video'; - #~ $entry->{id} = $entry->{contentDetails}{bulletin}{resourceId}{videoId}; - #~ } - #~ } - #~ } - -#<<< - #~ @$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 @video_ids = grep { defined } map { $yv_utils->get_video_id($_) } @{$videos}; - #~ my $content_details = $yv_obj->video_details(join(',', @video_ids), 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 { - #~ $yv_utils->get_video_id($_) - #~ and $yv_utils->get_time($_) ne '00:00' - #~ } @$videos; -#>>> - - my @formatted; - - foreach my $i (0 .. $#{$videos}) { - my $video = $videos->[$i]; - - #use Data::Dump qw(pp); - #pp $video; - - if ($opt{custom_layout}) { - - my $entry = $opt{custom_layout_format}; - - if (ref($entry) eq '') { - $entry =~ s/\*NO\*/sprintf('%2d', $i+1)/ge; - $entry = $yv_utils->format_text( - info => $video, - text => $entry, - escape => 0, - ); - push @formatted, "$entry\n"; - } - - if (ref($entry) eq 'ARRAY') { - - my @columns; - - foreach my $slot (@$entry) { - - my $text = $slot->{text}; - my $width = $slot->{width} // 10; - my $color = $slot->{color}; - my $align = $slot->{align} // 'left'; - - if ($width =~ /^(\d+)%\z/) { - $width = int(($term_width * $1) / 100); - } - - $text =~ s/\*NO\*/$i+1/ge; - - $text = $yv_utils->format_text( - info => $video, - text => $text, - escape => 0, - ); - - $text = clear_title($text); - $text = adjust_width($text, $width, ($align eq 'right')); - - if (defined($color)) { - $text = colored($text, $color); - } - - push @columns, $text; - } - - push @formatted, join(' ', @columns) . "\n"; - } - } - 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->get_time($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), 'blue'), - colored($yv_utils->get_time($video), 'magenta'), - colored($yv_utils->get_channel_title($video), 'green'), - ); - } - 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]; - my $title = clear_title($yv_utils->get_title($video)); - - push @formatted, - sprintf("%s. %s %s %*s\n", - colored(sprintf('%2d', $i + 1), 'bold'), - adjust_width($title, $title_length), - adjust_width($yv_utils->get_channel_title($video), $author_width, 1), - $time_width, $yv_utils->get_time($video)); - } - 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->get_time($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), auto => 1); - #} - #else { - # $opt{play_backwards} = 0; - # warn_first_page(); - # return; - #} - } - else { - #if (defined $info->{nextPageToken}) { - __SUB__->($yv_obj->next_page($url), 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); - __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), @keywords ? (auto => 1) : ()); - #} - #else { - # warn_first_page(); - #} - } - elsif ($opt =~ /^(?:R|refresh)\z/) { - @{$videos} = @{$yv_obj->_get_results($url)->{results}{items}}; - $results->{has_extra_info} = 0; - } - elsif ($opt =~ /^(?:r|return)\z/) { - return; - } - elsif ($opt =~ /^(?:a|author|u|uploads)${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 =~ /^(?:pv|popular)${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->popular_videos($channel_id); - if ($yv_utils->has_entries($request)) { - __SUB__->($request); - } - else { - warn_no_results('popular video'); - } - } - } - else { - warn_no_thing_selected('video'); - } - } - elsif ($opt =~ /^(?:A|[Aa]ctivity)${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->activities($channel_id); - if ($yv_utils->has_entries($request)) { - __SUB__->($request); - } - else { - warn_no_results('activity'); - } - } - } - else { - warn_no_thing_selected('activity'); - } - } - 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|playlists?|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|favorite|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(map { $yv_utils->get_channel_id($videos->[$_]) } @nums); - } - else { - warn_no_thing_selected('video'); - } - } - elsif ($opt =~ /^(?:q|queue|enqueue)${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 =~ /^(?:ap|autoplay)${digit_or_equal_re}(.*)/) { - if (my ($id) = get_valid_numbers($#{$videos}, $1)) { - autoplay($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) { - local $opt{show_video_info} = 1; - 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"); - if (@ids) { - push @for_play, @ids; - } - else { - push @for_search, $key; - } - } - else { - push @for_play, $key - 1; - } - - if (@for_play and 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); |