aboutsummaryrefslogtreecommitdiffstats
path: root/bin/fair-viewer
diff options
context:
space:
mode:
authorJesús <heckyel@hyperbola.info>2021-07-09 15:27:16 -0500
committerJesús <heckyel@hyperbola.info>2021-07-09 15:27:16 -0500
commit739c821a54c01816e60eb5f774c8977a1e221ea0 (patch)
treee04a7f5a6fe4d450d43fd45c412f9d415bcb7a7e /bin/fair-viewer
parentc1322a4e9a1fb0a286dab1277a740072d0ab30f9 (diff)
downloadfair-viewer-739c821a54c01816e60eb5f774c8977a1e221ea0.tar.lz
fair-viewer-739c821a54c01816e60eb5f774c8977a1e221ea0.tar.xz
fair-viewer-739c821a54c01816e60eb5f774c8977a1e221ea0.zip
upstream
Diffstat (limited to 'bin/fair-viewer')
-rwxr-xr-xbin/fair-viewer2014
1 files changed, 1234 insertions, 780 deletions
diff --git a/bin/fair-viewer b/bin/fair-viewer
index e97475c..127f1fe 100755
--- a/bin/fair-viewer
+++ b/bin/fair-viewer
@@ -1,7 +1,8 @@
#!/usr/bin/perl
-# Copyright (C) 2010-2020 Trizen <echo dHJpemVuQHByb3Rvbm1haWwuY29tCg== | base64 -d>.
-# Copyright (C) 2020 Jesus E. <echo aGVja3llbEBoeXBlcmJvbGEuaW5mbw== | base64 -d>.
+# Copyright (C) 2010-2021 Trizen <echo dHJpemVuQHByb3Rvbm1haWwuY29tCg== | base64 -d>.
+# Copyright (C) 2020-2021 Jesus E. <echo aGVja3llbEBoeXBlcmJvbGEuaW5mbw== | 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
@@ -15,9 +16,9 @@
#
#-------------------------------------------------------
# fair-viewer
-# Fork: 14 February 2020
-# Edit: 05 October 2020
-# https://framagit.org/heckyel/fair-viewer
+# Fork: 30 October 2020
+# Edit: 19 June 2021
+# https://git.sr.ht/~heckyel/fair-viewer
#-------------------------------------------------------
# fair-viewer is a command line utility for streaming YouTube videos in mpv/vlc.
@@ -31,26 +32,11 @@
fair-viewer - YouTube from command line.
-See: fair-viewer --help
+ fair-viewer --help
fair-viewer --tricks
fair-viewer --examples
fair-viewer --stdin-help
-=head1 LICENSE AND COPYRIGHT
-
-Copyright 2010-2020 Trizen.
-Copyright 2020 Jesus E.
-
-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;
@@ -59,21 +45,22 @@ use 5.016;
use warnings;
no warnings 'once';
-my $DEVEL; # true in devel mode
-use if ($DEVEL = 0), lib => qw(../lib); # devel mode
+my $DEVEL; # true in devel mode
+use if ($DEVEL = -w __FILE__), lib => qw(../lib); # devel mode
use WWW::FairViewer v1.0.6;
use WWW::FairViewer::RegularExpressions;
+require Storable;
+
use File::Spec::Functions qw(
catdir
catfile
curdir
path
rel2abs
- tmpdir
file_name_is_absolute
- );
+);
binmode(STDOUT, ':utf8');
@@ -90,7 +77,7 @@ my %opt;
my $term_width = 80;
# Keep track of watched videos by their ID
-my %watched_videos;
+my %WATCHED_VIDEOS;
# Unchangeable data goes here
my %constant = (win32 => ($^O eq 'MSWin32' ? 1 : 0)); # doh
@@ -119,18 +106,39 @@ else {
$xdg_config_home = catdir($home_dir, '.config');
}
-# Configuration dir/file
+# Configuration dirs
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': $!";
+my $local_playlists_dir = catdir($config_dir, 'playlists');
+
+# Configuration files
+my $config_file = catfile($config_dir, "$execname.conf");
+my $youtube_users_file = catfile($config_dir, 'users.txt');
+my $subscribed_channels_file = catfile($config_dir, 'subscribed_channels.txt');
+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');
+
+# Special local playlists
+my $watch_history_data_file = catfile($local_playlists_dir, 'watched_videos.dat');
+my $liked_videos_data_file = catfile($local_playlists_dir, 'liked_videos.dat');
+my $disliked_videos_data_file = catfile($local_playlists_dir, 'disliked_videos.dat');
+my $favorite_videos_data_file = catfile($local_playlists_dir, 'favorite_videos.dat');
+my $subscription_videos_data_file = catfile($local_playlists_dir, "subscriptions.dat");
+
+# Create the config and playlist dirs
+foreach my $dir ($config_dir, $local_playlists_dir) {
+ if (not -d $dir) {
+ require File::Path;
+ File::Path::make_path($dir)
+ or warn "[!] Can't create dir <<$dir>>: $!";
+ }
+}
+
+# Create the special playlist files
+foreach my $file ($watch_history_data_file, $liked_videos_data_file, $disliked_videos_data_file, $favorite_videos_data_file,) {
+ if (not -s $file) {
+ Storable::store([], $file);
+ }
}
sub which_command {
@@ -179,10 +187,10 @@ my %CONFIG = (
: undef # auto-defined
),
+ split_videos => 1,
+
# YouTube options
- dash_support => 1,
- dash_mp4_audio => 1,
- dash_segmented => 1, # may load slow
+ dash => 1, # may load slow
maxResults => 20,
hfr => 1, # true to prefer high frame rate (HFR) videos
resolution => 'best',
@@ -192,14 +200,13 @@ my %CONFIG = (
videoCaption => undef,
videoDuration => undef,
- order => undef,
- date => undef,
-
- comments_order => 'top', # valid values: top, new
- subscriptions_order => 'relevance', # valid values: alphabetical, relevance, unread
-
+ order => undef,
+ date => undef,
region => undef,
+ # Comments order
+ comments_order => 'top', # valid values: top, new
+
# URI options
youtube_video_url => 'https://www.youtube.com/watch?v=%s',
@@ -213,59 +220,85 @@ my %CONFIG = (
# API
api_host => "auto",
- # Others
- autoplay_mode => 0,
- http_proxy => undef,
- cookie_file => undef,
- user_agent => undef,
- timeout => undef,
- env_proxy => 1,
- confirm => 0,
- debug => 0,
- page => 1,
- colors => $constant{win32} ^ 1,
- skip_if_exists => 1,
- prefer_mp4 => 0,
- prefer_av1 => 0,
- ignore_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,
- 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,
+ # Misc options
+ autoplay_mode => 0,
+ http_proxy => undef,
+ cookie_file => undef,
+ user_agent => undef,
+ timeout => undef,
+ env_proxy => 1,
+ confirm => 0,
+ debug => 0,
+ page => 1,
+ colors => $constant{win32} ^ 1,
+ skip_if_exists => 1,
+ prefer_mp4 => 0,
+ prefer_m4a => 0,
+ prefer_av1 => 0,
+ ignore_av1 => 0,
+ prefer_invidious => 0,
+ fat32safe => $constant{win32},
+ fullscreen => 0,
+ show_video_info => 1,
+ interactive => 1,
+ get_term_width => $constant{win32} ^ 1,
+ download_with_wget => undef, # auto-defined
+ thousand_separator => q{,},
+ downloads_dir => curdir(),
+ download_and_play => 0,
+ remove_played_file => 0,
+
+ ignored_projections => [],
+
+ # Conversion options
+ convert_cmd => 'ffmpeg -i *IN* *OUT*',
+ convert_to => undef,
+ keep_original_video => 0,
+
+ # Search history
+ history => undef, # auto-defined
+ history_limit => 100_000,
+ history_file => $history_file,
+
+ # Watch history
+ watch_history => 1,
+ watched_file => $watched_file,
+
+ # Subscribed channels
+ subscribed_channels_file => $subscribed_channels_file,
+ subscriptions_limit => 10_000, # maximum number of subscription videos stored
+ youtube_users_file => $youtube_users_file,
+
+ # Options for watched videos
+ highlight_watched => 1,
+ highlight_color => 'bold',
+ skip_watched => 0,
# hypervideo support
ytdl => 1,
- ytdl_cmd => undef, # auto-defined
+ ytdl_cmd => undef, # auto-defined
- custom_layout => undef, # auto-defined
+ # Custom layout
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 => "15%", align => "left", color => "magenta", 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*",},
],
+ custom_channel_layout_format => [{width => 3, align => "right", color => "bold", text => "*NO*.",},
+ {width => "55%", align => "left", color => "bold blue", text => "*AUTHOR*",},
+ {width => 14, align => "right", color => "green", text => "*VIDEOS* videos",},
+ {width => 10, align => "right", color => "green", text => "*SUBS_SHORT* subs",},
+ ],
+
+ custom_playlist_layout_format => [{align => "right", color => "bold", text => "*NO*.", width => 3},
+ {align => "left", color => "bold blue", text => "*TITLE*", width => "55%"},
+ {align => "right", color => "green", text => "*ITEMS* videos", width => 14},
+ {align => "left", color => "magenta", text => "*AUTHOR*", width => "20%"},
+ ],
+
ffmpeg_cmd => 'ffmpeg',
wget_cmd => 'wget',
@@ -297,13 +330,12 @@ ACTIONS
my $control_options = <<'CONTROL';
# Control
-:n(ext) : get the next page of results
-:b(ack) : get the previous page of results
+:n(ext) : display the next page of results
+:r(eturn) : return to 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)
@@ -332,17 +364,32 @@ Examples:
-sp classical music : search for playlists
HELP
-my $playlists_help = <<"PL_HELP" . $general_help;
+my $playlists_help = <<"PLAYLISTS_HELP" . $general_help;
-# Playlists
+# Select a playlist
+<number> : list videos from the selected playlist
:pp=i,i : play videos from the selected playlists
-PL_HELP
+PLAYLISTS_HELP
+
+my $channels_help = <<"CHANNELS_HELP" . $general_help;
-my $comments_help = <<"COM_HELP" . $general_help;
+# Select a channel
+<number> : latest uploads from channel
+:pv=i :popular=i : popular uploads from channel
+:p=i :playlists=i : playlists from channel
+
+# Save and remove channels
+ :save=i : save channel
+:s=i :subscribe=i : subscribe to the channel
+ :unsub=i : unsubscribe from the channel
+:r=i :remove=i : remove the channel
+CHANNELS_HELP
+
+my $comments_help = <<"COMMENTS_HELP" . $general_help;
# Comments
:c(omment) : send a comment to this video
-COM_HELP
+COMMENTS_HELP
my $complete_help = <<"STDIN_HELP";
@@ -356,9 +403,7 @@ $action_options
: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
@@ -450,7 +495,7 @@ sub load_config {
$update_config = 1;
}
- # Locating a video player
+ # Locate video player
if (not $CONFIG{video_player_selected}) {
foreach my $key (sort keys %{$CONFIG{video_players}}) {
@@ -515,23 +560,6 @@ sub load_config {
$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}) {
@@ -554,6 +582,7 @@ sub load_config {
dump_configuration($config_file) if $update_config;
+ # Create the cache directory (if needed)
foreach my $path ($CONFIG{cache_dir}) {
next if -d $path;
require File::Path;
@@ -566,11 +595,11 @@ sub load_config {
load_config($config_file);
-if ($opt{remember_watched}) {
+if ($opt{watch_history}) {
if (-f $opt{watched_file}) {
if (open my $fh, '<', $opt{watched_file}) {
chomp(my @ids = <$fh>);
- @watched_videos{@ids} = ();
+ @WATCHED_VIDEOS{@ids} = ();
close $fh;
}
else {
@@ -601,7 +630,8 @@ if ($opt{history}) {
eval { $term->ReadHistory($opt{history_file}) };
# All history entries
- my @history = $term->history_list;
+ my @history;
+ eval { @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}) {
@@ -620,21 +650,21 @@ if ($opt{history}) {
}
my $yv_obj = WWW::FairViewer->new(
- escape_utf8 => 1,
- config_dir => $config_dir,
- ytdl => $opt{ytdl},
- ytdl_cmd => $opt{ytdl_cmd},
- cache_dir => $opt{cache_dir},
- env_proxy => $opt{env_proxy},
- cookie_file => $opt{cookie_file},
- http_proxy => $opt{http_proxy},
- user_agent => $opt{user_agent},
- timeout => $opt{timeout},
- );
+ escape_utf8 => 1,
+ config_dir => $config_dir,
+ ytdl => $opt{ytdl},
+ ytdl_cmd => $opt{ytdl_cmd},
+ cache_dir => $opt{cache_dir},
+ env_proxy => $opt{env_proxy},
+ cookie_file => $opt{cookie_file},
+ http_proxy => $opt{http_proxy},
+ user_agent => $opt{user_agent},
+ timeout => $opt{timeout},
+ );
require WWW::FairViewer::Utils;
my $yv_utils = WWW::FairViewer::Utils->new(youtube_url_format => $opt{youtube_video_url},
- thousand_separator => $opt{thousand_separator},);
+ thousand_separator => $opt{thousand_separator},);
{ # Apply the configuration file
my %temp = %CONFIG;
@@ -672,18 +702,16 @@ usage: $execname [options] ([url] | [keywords])
-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)
+ -wv --watched-videos : list the most recent watched videos
+ -ls --local-subs : display subscription videos from local channels
* 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
+ -up --playlists=s : list playlists created by a specific channel or user
+ -lp --local-playlists : display the list of local playlists
+ -lp=s : display a local playlist by name
+ -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
* Trending
--trending:s : show trending videos in a given category
@@ -704,7 +732,7 @@ usage: $execname [options] ([url] | [keywords])
--captions! : only videos with or without closed captions
--order=s : order the results using a specific sorting method
valid values: relevance rating upload_date view_count
- --date=s : short videos published in a time period
+ --time=s : short videos published in a time period
valid values: hour today week month year
--hd! : search only for videos available in at least 720p
--vd=s : set the video definition (any or high)
@@ -714,6 +742,8 @@ usage: $execname [options] ([url] | [keywords])
--results=i : how many results to display per page (max: 50)
--hfr! : prefer high frame rate (HFR) videos
-2 -3 -4 -7 -1 : resolutions: 240p, 360p, 480p, 720p and 1080p
+ -a --audio : prefer audio part only (implied by --novideo)
+ --best : prefer best resolution available
--resolution=s : supported resolutions: best, 2160p, 1440p,
1080p, 720p, 480p, 360p, 240p, 144p, audio.
@@ -721,29 +751,27 @@ usage: $execname [options] ([url] | [keywords])
--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 *
+ * Display local
+ -P --playlists : show the local playlists
+ -F --favorites : show the local favorite videos
+ -lc --saved-channels : show the saved channels
+ -S --subscriptions : show the subscribed channels
+ -L --likes : show the videos that you liked
+ -D --dislikes : show the videos that you disliked
-* [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 *
+* Save local
+ --save=s : save a given channel ID or username in -lc
+ --subscribe=s : subscribe to a given channel ID or username
+ --favorite=s : favorite a video by URL or ID
+ --like=s : like a video by URL or ID (see: -L)
+ --dislike=s : dislike a video by URL or ID (see: -D)
== Player Options ==
* Arguments
-f --fullscreen! : play videos in fullscreen mode
- -n --audio! : play audio only, without displaying video
+ -n --novideo : 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}}]}
@@ -786,23 +814,21 @@ usage: $execname [options] ([url] | [keywords])
--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
+ --prefer-mp4! : prefer videos in MP4 format, instead of VP9
+ --prefer-av1! : prefer videos in AV1 format, instead of VP9
+ --prefer-m4a! : prefer audios in AAC format, instead of OPUS
--ignore-av1! : ignore videos in AV1 format
* Closed-captions
--get-captions! : download closed-captions for videos
--auto-captions! : include or exclude auto-generated captions
+ --srt-languages=s : comma separated list of preferred languages
* 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
@@ -814,17 +840,22 @@ usage: $execname [options] ([url] | [keywords])
--escape-info! : quotemeta() the fields of the `--extract`
--use-colors! : enable or disable the ANSI colors for text
+* Formatting
+ --custom-layout=s : custom layout format for videos
+ --custom-channel-layout=s : custom layout format for channels
+ --custom-playlist-layout=s : custom layout format for playlists
+
* Other
- --api=s : set an API host from https://instances.invidio.us/
+ --invidious! : prefer invidious instances over parsing YouTube
+ --api=s : set an API host from https://api.invidious.io/
--api=auto : use a random instance of invidious
--cookies=s : file to read cookies from and dump cookie
--user-agent=s : specify a custom user agent
--proxy=s : set HTTP(S)/SOCKS proxy: 'proto://domain.tld:port/'
If authentication is 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
+ --split-videos! : include or exclude the itags for split videos
+ --dash! : include or exclude segmented DASH streams
--ytdl! : use hypervideo for videos with encrypted signatures
`--no-ytdl` will use invidious instances
--ytdl-cmd=s : hypervideo command (default: hypervideo)
@@ -838,7 +869,6 @@ Help options:
--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
@@ -890,7 +920,6 @@ sub tricks {
> ":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'.
@@ -919,26 +948,32 @@ sub tricks {
-> 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
+ *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
+ *VIDEOS* : the number of channel videos
+ *VIDEOS_SHORT* : the number of channel videos in abbreviated notation
+ *SUBS* : the number of channel subscriptions
+ *SUBS_SHORT* : the number of channel subscriptions in abbreviated notation
+ *ITEMS* : the number of playlist items
+ *ITEMS_SHORT* : the number of playlist items 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
@@ -980,33 +1015,32 @@ sub examples {
print <<"EXAMPLES";
==== COMMAND LINE EXAMPLES ====
-Command: $execname -A -n russian music -category=10
+Command: $execname -A -n russian music
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.
+Note: -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 --results=5 -up=khanacademy
+Results: the most recent 5 playlists by a specific author (-up).
+
+Command: $execname --author=MIT atom
+Results: search only in videos by a specific author.
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=view_count --duration=short
Results: search for 'cats' videos, ordered by ViewCount and short duration.
-Command: $execname --channels math lessons
+Command: $execname -sc math lessons
Results: search for YouTube channels.
Command: $execname -uf=Google
-Results: show latest videos favorited by a user.
+Results: show the latest favorite videos by a user.
==== USER INPUT EXAMPLES ====
@@ -1016,8 +1050,8 @@ 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: :r, :return
+Results: return to 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.
@@ -1106,13 +1140,13 @@ sub apply_configuration {
qw(
videoCaption videoDefinition
videoDimension videoDuration
- videoLicense
+ videoLicense maxResults
api_host date order
channelId region debug
http_proxy page comments_order
- subscriptions_order user_agent
+ user_agent
cookie_file timeout ytdl ytdl_cmd
- prefer_mp4 prefer_av1
+ prefer_mp4 prefer_av1 prefer_invidious
)
) {
@@ -1142,13 +1176,6 @@ sub apply_configuration {
};
}
- 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 {
@@ -1156,10 +1183,6 @@ sub apply_configuration {
}
}
- if (defined $opt->{more_results}) {
- $yv_obj->set_maxResults(delete($opt->{more_results}) ? 50 : $CONFIG{maxResults});
- }
-
if (delete $opt->{authenticate}) {
authenticate();
}
@@ -1191,7 +1214,15 @@ sub apply_configuration {
# ... SUBROUTINE CALLS ... #
if (defined $opt->{subscribe}) {
- subscribe(split(/[,\s]+/, delete $opt->{subscribe}));
+ foreach my $channel_id (split(/[,\s]+/, delete $opt->{subscribe})) {
+ subscribe_channel($channel_id);
+ }
+ }
+
+ if (defined $opt->{save_channel}) {
+ foreach my $channel_id (split(/[,\s]+/, delete $opt->{save_channel})) {
+ save_channel($channel_id);
+ }
}
if (defined $opt->{favorite_video}) {
@@ -1224,6 +1255,18 @@ sub apply_configuration {
get_and_play_playlists(split(/[,\s]+/, delete $opt->{play_playlists}));
}
+ if (defined $opt->{saved_channels}) {
+ print_saved_channels(delete $opt->{saved_channels});
+ }
+
+ if (defined $opt->{subscribed_channels}) {
+ print_subscribed_channels(delete $opt->{subscribed_channels});
+ }
+
+ if (defined $opt->{local_playlist}) {
+ print_local_playlist(delete $opt->{local_playlist});
+ }
+
if (defined $opt->{playlist_id}) {
my $playlistID = get_valid_playlist_id(delete($opt->{playlist_id})) // return;
get_and_print_videos_from_playlist($playlistID);
@@ -1245,6 +1288,14 @@ sub apply_configuration {
print_categories($yv_obj->video_categories);
}
+ if (delete $opt->{watched_videos}) {
+ print_watched_videos();
+ }
+
+ if (delete $opt->{local_subscriptions}) {
+ print_local_subscription_videos();
+ }
+
if (defined $opt->{uploads}) {
my $str = delete $opt->{uploads};
@@ -1259,14 +1310,17 @@ sub apply_configuration {
}
}
else {
- print_videos($yv_obj->uploads);
+ warn_invalid("username or channel ID", $str);
}
}
if (defined $opt->{popular_videos}) {
my $str = delete $opt->{popular_videos};
- if (my $id = extract_channel_id($str)) {
+ if ($str eq '') {
+ print_videos($yv_obj->trending_videos_from_category('popular'));
+ }
+ elsif (my $id = extract_channel_id($str)) {
if (not $yv_utils->is_channelID($id)) {
$id = $yv_obj->channel_id_from_username($id) // do {
@@ -1275,13 +1329,6 @@ sub apply_configuration {
};
}
- 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 {
@@ -1294,42 +1341,6 @@ sub apply_configuration {
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})));
}
@@ -1349,7 +1360,7 @@ sub apply_configuration {
}
}
else {
- print_playlists($yv_obj->my_playlists);
+ print_local_playlist();
}
}
@@ -1367,49 +1378,16 @@ sub apply_configuration {
}
}
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);
+ print_favorite_videos();
}
}
- if (defined $opt->{dislikes}) {
- delete $opt->{dislikes};
- print_videos($yv_obj->my_dislikes);
+ if (defined delete $opt->{likes}) {
+ print_liked_videos();
}
- 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 delete $opt->{dislikes}) {
+ print_disliked_videos();
}
if (defined $opt->{get_comments}) {
@@ -1445,11 +1423,16 @@ sub parse_arguments {
'update-config!' => sub { dump_configuration($config_file) },
# Resolutions
+ 'audio|a' => sub { $opt{resolution} = 'audio' },
+ '144p' => sub { $opt{resolution} = 144 },
'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 },
+ '1440p' => sub { $opt{resolution} = 1440 },
+ '2160p' => sub { $opt{resolution} = 2160 },
+ 'best' => sub { $opt{resolution} = 'best' },
'hfr!' => \$opt{hfr},
'res|resolution=s' => \$opt{resolution},
@@ -1460,19 +1443,26 @@ sub parse_arguments {
'c|categories' => \$opt{categories},
'video-ids|videoids|id|ids=s' => \$opt{play_video_ids},
+ 'lc|fc|local-channels|saved-channels:s' => \$opt{saved_channels},
+ 'subscriptions|sub-channels|S:s' => \$opt{subscribed_channels},
+ 'lp|local-playlists:s' => \$opt{local_playlist},
+ 'wv|watched-videos' => \$opt{watched_videos},
+ 'ls|local-subs|sub-videos|SV' => \$opt{local_subscriptions},
+
+ #'save-video|save=s' => \$opt{save_video},
+ 'save|save-channel=s' => \$opt{save_channel},
+
+ #'save-playlist=s' => \$opt{save_playlist},
+
'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},
+ '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},
+ 'likes|L|user-likes' => \$opt{likes},
+ 'dislikes|D' => \$opt{dislikes},
'subscribe=s' => \$opt{subscribe},
'trending|trends:s' => \$opt{trending},
@@ -1485,7 +1475,7 @@ sub parse_arguments {
'logout' => \$opt{logout},
'related-videos|rv=s' => \$opt{related_videos},
- 'popular-videos|popular|pv=s' => \$opt{popular_videos},
+ 'popular-videos|popular|pv:s' => \$opt{popular_videos},
'cookie-file|cookies=s' => \$opt{cookie_file},
'user-agent|agent=s' => \$opt{user_agent},
@@ -1494,7 +1484,7 @@ sub parse_arguments {
'r|region|region-code=s' => \$opt{region},
'order|order-by|sort|sort-by=s' => \$opt{order},
- 'date=s' => \$opt{date},
+ 'time|date=s' => \$opt{date},
'duration=s' => \$opt{videoDuration},
@@ -1548,43 +1538,42 @@ sub parse_arguments {
'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},
- 'captions!' => \$opt{videoCaption},
- 'fullscreen|fs|f!' => \$opt{fullscreen},
- 'dash!' => \$opt{dash_support},
- 'confirm!' => \$opt{confirm},
+ 'captions!' => \$opt{videoCaption},
+ 'fullscreen|fs|f!' => \$opt{fullscreen},
+ 'split-videos!' => \$opt{split_videos},
+ 'confirm!' => \$opt{confirm},
'prefer-mp4!' => \$opt{prefer_mp4},
'prefer-av1!' => \$opt{prefer_av1},
'ignore-av1!' => \$opt{ignore_av1},
- 'custom-layout!' => \$opt{custom_layout},
- 'custom-layout-format=s' => \$opt{custom_layout_format},
+ 'invidious|prefer-invidious!' => \$opt{prefer_invidious},
+
+ 'custom-layout-format=s' => \$opt{custom_layout_format},
+ 'custom-channel-layout-format=s' => \$opt{custom_channel_layout_format},
+ 'custom-playlist-layout-format=s' => \$opt{custom_playlist_layout_format},
'merge-into-mkv|mkv-merge!' => \$opt{merge_into_mkv},
'merge-with-captions|merge-captions!' => \$opt{merge_with_captions},
'api-host|instance=s' => \$opt{api_host},
- '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},
- '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},
+ 'convert-command|convert-cmd=s' => \$opt{convert_cmd},
+ 'prefer-m4a!' => \$opt{prefer_m4a},
+ 'dash|dash-segmented!' => \$opt{dash},
+ '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' => \$opt{novideo},
+ 'highlight!' => \$opt{highlight_watched},
+ 'skip-watched!' => \$opt{skip_watched},
+ 'results=i' => \$opt{maxResults},
+ 'shuffle|s!' => \$opt{shuffle},
+ 'pos|position=i' => \$opt{position},
+ 'ps|playlist-save=s' => \$opt{playlist_save},
'ytdl!' => \$opt{ytdl},
'ytdl-cmd=s' => \$opt{ytdl_cmd},
@@ -1598,6 +1587,7 @@ sub parse_arguments {
'thousand-separator=s' => \$opt{thousand_separator},
'get-captions|get_captions!' => \$opt{get_captions},
'auto-captions|auto_captions!' => \$opt{auto_captions},
+ 'srt-languages=s' => \$opt{srt_languages},
'copy-caption|copy_caption!' => \$opt{copy_caption},
'skip-if-exists|skip_if_exists!' => \$opt{skip_if_exists},
'downloads-dir|download-dir=s' => \$opt{downloads_dir},
@@ -1789,6 +1779,7 @@ sub get_quotewords {
sub clear_title {
my ($title) = @_;
+ $title //= "";
$title =~ s/[^\w\s[:punct:]]//g;
$title = join(' ', split(' ', $title));
@@ -1909,7 +1900,7 @@ sub get_user_input {
sub logout {
unlink $authentication_file
- or warn "Can't unlink: `$authentication_file' -> $!";
+ or warn "[!] Can't unlink: `$authentication_file' -> $!";
$yv_obj->set_access_token();
$yv_obj->set_refresh_token();
@@ -1947,7 +1938,7 @@ INFO
if ($remember_me) {
$yv_obj->set_authentication_file($authentication_file);
$yv_obj->save_authentication_tokens()
- or warn "Can't store the authentication tokens: $!";
+ or warn "[!] Can't store the authentication tokens: $!";
}
else {
$yv_obj->set_authentication_file();
@@ -1969,19 +1960,12 @@ sub authenticated {
}
sub favorite_videos {
- my (@videoIDs) = @_;
- return if not authenticated();
+ my (@videos) = @_;
- 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);
- }
+ foreach my $video_data (@videos) {
+ prepend_video_data_to_file($video_data, $favorite_videos_data_file);
}
+
return 1;
}
@@ -2006,6 +1990,7 @@ sub save_to_playlist {
return if not authenticated();
foreach my $id (@videoIDs) {
+
my $videoID = get_valid_video_id($id) // next;
my $pos = $opt{position}; # position in the playlist
@@ -2034,16 +2019,11 @@ sub save_to_playlist {
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);
- }
+ my $file = ($rating eq 'like') ? $liked_videos_data_file : $disliked_videos_data_file;
+
+ foreach my $video_data (@_) {
+ prepend_video_data_to_file($video_data, $file);
}
return 1;
@@ -2132,33 +2112,6 @@ sub get_and_print_videos_from_playlist {
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;
}
@@ -2220,7 +2173,7 @@ sub general_options {
if ($option =~ /^(?:q|quit|exit)\z/) {
main_quit(0);
}
- elsif ($option =~ /^(?:n|next)\z/ and defined $url) {
+ elsif ($option =~ /^(?:n|next)\z/ and (defined($url) or ref($token) eq 'CODE')) {
if ($has_token) {
if (defined $token) {
my $request = $yv_obj->next_page($url, $token);
@@ -2235,11 +2188,7 @@ sub general_options {
$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) {
+ elsif ($option =~ /^(?:R|refresh)\z/ and defined($url)) {
@{$results} = @{$yv_obj->_get_results($url)->{results}};
}
elsif ($option eq 'login') {
@@ -2312,7 +2261,7 @@ sub warn_last_page {
}
sub warn_first_page {
- warn colored("\n[!] No previous page available...", 'bold red') . "\n";
+ warn colored("\n[!] This is the first page!", 'bold red') . "\n";
}
sub warn_no_thing_selected {
@@ -2408,52 +2357,109 @@ sub adjust_width {
}
);
- #
- ## Unicode::GCString
- #
- if ($pkg eq 'Unicode::GCString') {
+ my $adjust_str = sub {
+
+ # Unicode::GCString
+ if ($pkg eq 'Unicode::GCString') {
- my $gcstr = Unicode::GCString->new($str);
- my $str_width = $gcstr->columns;
+ 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, $str_width);
}
- return $str;
- }
-
- #
- ## Text::CharWidth
- #
- if ($pkg eq 'Text::CharWidth') {
+ # Text::CharWidth
+ if ($pkg eq 'Text::CharWidth') {
- my $str_width = Text::CharWidth::mbswidth($str);
+ 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, $str_width);
}
- return $str;
+ # Fallback to counting graphemes
+ my @graphemes = $str =~ /(\X)/g;
+
+ while (scalar(@graphemes) > $len) {
+ pop @graphemes;
+ }
+
+ $str = join('', @graphemes);
+ return ($str, scalar(@graphemes));
+ };
+
+ my ($new_str, $str_width) = $adjust_str->();
+
+ my $spaces = ' ' x ($len - $str_width);
+ my $result = $prepend ? join('', $spaces, $new_str) : join('', $new_str, $spaces);
+
+ return $result;
+}
+
+sub format_line_result {
+ my ($i, $entry, $info, %args) = @_;
+
+ if (ref($entry) eq '') {
+ $entry =~ s/\*NO\*/sprintf('%2d', $i+1)/ge;
+ $entry = $yv_utils->format_text(
+ info => $info,
+ text => $entry,
+ escape => 0,
+ );
+ return "$entry\n";
}
- return $str;
+ 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 => $info,
+ 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;
+ }
+
+ return (join(' ', @columns) . "\n");
+ }
+
+ die "ERROR: invalid custom layout format <<$entry>>\n";
}
# ... PRINT SUBROUTINES ... #
+
sub print_channels {
my ($results) = @_;
@@ -2461,66 +2467,38 @@ sub print_channels {
warn_no_results("channel");
}
- if ($opt{get_term_width} and $opt{results_fixed_width}) {
+ if ($opt{get_term_width}) {
get_term_width();
}
my $url = $results->{url};
my $channels = $results->{results} // [];
- foreach my $i (0 .. $#{$channels}) {
- my $channel = $channels->[$i];
+ if (ref($channels) eq 'HASH') {
+ if (exists $channels->{channels}) {
+ $channels = $channels->{channels};
+ }
+ elsif (exists $channels->{entries}) {
+ $channels = $channels->{entries};
+ }
+ else {
+ warn "\n[!] No channels...\n";
+ $channels = [];
+ }
+ }
- 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);
+ my @formatted;
- print "\n";
- foreach my $i (0 .. $#{$channels}) {
+ foreach my $i (0 .. $#{$channels}) {
- my $channel = $channels->[$i];
- my $title = clear_title($yv_utils->get_channel_title($channel));
+ my $channel = $channels->[$i];
+ my $entry = $opt{custom_channel_layout_format};
- 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_channel_title($channel),
- $yv_utils->get_publication_date($channel);
- }
+ push @formatted, format_line_result($i, $entry, $channel);
+ }
+
+ if (@formatted) {
+ print "\n" . join("", @formatted);
}
my @keywords = get_input_for_channels();
@@ -2542,13 +2520,122 @@ sub print_channels {
) {
## ok
}
+
+ # :h, :help
elsif ($opt =~ /^(?:h|help)\z/) {
- print $general_help;
+ print $channels_help;
press_enter_to_continue();
}
+
+ # :r, :return
elsif ($opt =~ /^(?:r|return)\z/) {
return;
}
+
+ # :i=i, :info=i
+ elsif ($opt =~ /^(?:i|info)${digit_or_equal_re}(.*)/) {
+ if (my @ids = get_valid_numbers($#{$channels}, $1)) {
+ foreach my $id (@ids) {
+ print_channel_info($channels->[$id]);
+ }
+ press_enter_to_continue();
+ }
+ else {
+ warn_no_thing_selected('playlist');
+ }
+ }
+
+ # :pv=i, :popular=i
+ elsif ($opt =~ /^(?:pv|popular)${digit_or_equal_re}(.*)/) {
+ if (my @nums = get_valid_numbers($#{$channels}, $1)) {
+
+ foreach my $id (@nums) {
+
+ my $channel_id = $yv_utils->get_channel_id($channels->[$id]);
+ my $request = $yv_obj->popular_videos($channel_id);
+
+ if ($yv_utils->has_entries($request)) {
+ print_videos($request);
+ }
+ else {
+ warn_no_results('popular video');
+ }
+ }
+ }
+ else {
+ warn_no_thing_selected('channel');
+ }
+ }
+
+ # :p=i, :playlist=i, :up=i
+ elsif ($opt =~ /^(?:p|l|playlists?|up)${digit_or_equal_re}(.*)/) {
+ if (my @nums = get_valid_numbers($#{$channels}, $1)) {
+
+ foreach my $id (@nums) {
+
+ my $channel_id = $yv_utils->get_channel_id($channels->[$id]);
+ my $request = $yv_obj->playlists($channel_id);
+
+ if ($yv_utils->has_entries($request)) {
+ print_playlists($request);
+ }
+ else {
+ warn_no_results('playlist');
+ }
+ }
+ }
+ else {
+ warn_no_thing_selected('channel');
+ }
+ }
+
+ # :s=i, :subscribe=i
+ elsif ($opt =~ /^(?:s|sub(?:scribe)?)${digit_or_equal_re}(.*)/) {
+ if (my @nums = get_valid_numbers($#{$channels}, $1)) {
+ foreach my $id (@nums) {
+ my $channel_id = $yv_utils->get_channel_id($channels->[$id]);
+ my $channel_title = $yv_utils->get_channel_title($channels->[$id]);
+ subscribe_channel($channel_id, $channel_title);
+ }
+ }
+ else {
+ warn_no_thing_selected('channel');
+ }
+ }
+
+ # :save=i
+ elsif ($opt =~ /^(?:save)${digit_or_equal_re}(.*)/) {
+ if (my @nums = get_valid_numbers($#{$channels}, $1)) {
+ foreach my $id (@nums) {
+ my $channel_id = $yv_utils->get_channel_id($channels->[$id]);
+ my $channel_title = $yv_utils->get_channel_title($channels->[$id]);
+ save_channel($channel_id, $channel_title);
+ }
+ }
+ else {
+ warn_no_thing_selected('channel');
+ }
+ }
+
+ # :r=i, :rm=i, :remove=i, :unsubscribe=i
+ elsif ($opt =~ /^(?:r|rm|remove)${digit_or_equal_re}(.*)/) {
+ if (my @nums = get_valid_numbers($#{$channels}, $1)) {
+ remove_saved_channels(map { $yv_utils->get_channel_id($channels->[$_]) } @nums);
+ }
+ else {
+ warn_no_thing_selected('channel');
+ }
+ }
+
+ # :unsub=i
+ elsif ($opt =~ /^(?:unsub(?:scribe)?)${digit_or_equal_re}(.*)/) {
+ if (my @nums = get_valid_numbers($#{$channels}, $1)) {
+ unsubscribe_from_channels(map { $yv_utils->get_channel_id($channels->[$_]) } @nums);
+ }
+ else {
+ warn_no_thing_selected('channel');
+ }
+ }
else {
warn_invalid('option', $opt);
}
@@ -2688,6 +2775,363 @@ sub print_comments {
__SUB__->(@_);
}
+sub _add_channel_to_file {
+ my ($channel_id, $channel_title, $file) = @_;
+
+ $channel_id // return;
+
+ if ($channel_id = extract_channel_id($channel_id)) {
+ if (not $yv_utils->is_channelID($channel_id)) {
+ $channel_id = $yv_obj->channel_id_from_username($channel_id) // do {
+ warn_invalid("username or channel ID", $channel_id);
+ undef;
+ };
+ }
+ }
+
+ $channel_id // return;
+ $channel_title //= $yv_obj->channel_title_from_id($channel_id) // $channel_id;
+
+ if (not defined($channel_title)) {
+ warn "[!] Could not determine the channel name...\n";
+ return;
+ }
+
+ say ":: Saving channel <<$channel_title>> (id: $channel_id) to file..." if $yv_obj->get_debug;
+
+ open(my $fh, '>>:utf8', $file) or do {
+ warn "[!] Can't open file <<$file>> for appending: $!\n";
+ return;
+ };
+
+ say $fh "$channel_id $channel_title";
+
+ close $fh;
+}
+
+sub save_channel {
+ my ($channel_id, $channel_title) = @_;
+ _add_channel_to_file($channel_id, $channel_title, $opt{youtube_users_file});
+}
+
+sub subscribe_channel {
+ my ($channel_id, $channel_title) = @_;
+ _add_channel_to_file($channel_id, $channel_title, $opt{youtube_users_file});
+ _add_channel_to_file($channel_id, $channel_title, $opt{subscribed_channels_file});
+}
+
+sub update_channel_file {
+ my ($channels, $file) = @_;
+
+ open(my $fh, '>:utf8', $file) or do {
+ warn "[!] Can't open file <<$file>> for writing: $!\n";
+ return;
+ };
+
+ foreach my $key (sort { CORE::fc($channels->{$a}) cmp CORE::fc($channels->{$b}) } keys %$channels) {
+ say $fh "$key $channels->{$key}";
+ }
+
+ close $fh;
+}
+
+sub _remove_channels_from_file {
+ my ($channel_ids, $file) = @_;
+
+ open(my $fh, '<:utf8', $file) or do {
+ warn "[!] Can't open file <<$file>> for reading: $!\n";
+ return;
+ };
+
+ my %channels;
+
+ while (defined(my $line = <$fh>)) {
+ if ($line =~ /^([a-zA-Z0-9_-]+) (.+)/) {
+ my ($channel_id, $channel_title) = ($1, $2);
+
+ if ($yv_utils->is_channelID($channel_id)) {
+ $channels{$channel_id} = $channel_title;
+ }
+ else {
+ warn "[!] Invalid channel ID: $channel_id\n";
+ }
+ }
+ }
+
+ close $fh;
+
+ my $removed = 0;
+
+ foreach my $channel_id (@$channel_ids) {
+ if (exists $channels{$channel_id}) {
+ say ":: Removing: $channel_id" if $yv_obj->get_debug;
+ delete $channels{$channel_id};
+ ++$removed;
+ }
+ else {
+ say ":: $channel_id is not a saved channel..." if $yv_obj->get_debug;
+ }
+ }
+
+ if ($removed > 0) {
+ update_channel_file(\%channels, $file);
+ }
+}
+
+sub remove_saved_channels {
+ my (@channel_ids) = @_;
+ _remove_channels_from_file(\@channel_ids, $opt{youtube_users_file});
+ _remove_channels_from_file(\@channel_ids, $opt{subscribed_channels_file});
+}
+
+sub unsubscribe_from_channels {
+ my (@channel_ids) = @_;
+ _remove_channels_from_file(\@channel_ids, $opt{subscribed_channels_file});
+}
+
+sub get_results_from_list {
+ my ($results, %args) = @_;
+
+ $args{page} //= $yv_obj->get_page;
+
+ if (ref($results) ne 'ARRAY') {
+ return;
+ }
+
+ my @results = @$results;
+
+ my $maxResults = $yv_obj->get_maxResults;
+ my $totalResults = scalar(@results);
+
+ if ($args{page} >= 1 and scalar(@results) >= $maxResults) {
+
+ @results = grep { defined } @results[($args{page} - 1) * $maxResults .. $args{page} * $maxResults - 1];
+
+ if (!@results) {
+ warn_last_page() if ($args{page} == 1 + sprintf('%0.f', 0.5 + $totalResults / $maxResults));
+ return __SUB__->($results, %args, page => $args{page} - 1) if ($args{page} > 1);
+ }
+ }
+
+ my %results;
+ my @entries;
+
+ foreach my $entry (@results) {
+ if (defined($args{callback})) {
+ push @entries, $args{callback}($entry);
+ }
+ else {
+ push @entries, $entry;
+ }
+ }
+
+#<<<
+ $results{entries} = \@entries;
+ #$results{pageInfo} = {resultsPerPage => scalar(@entries), totalResults => $totalResults};
+ #$results{fromPage} = sub { get_results_from_list($results, %args, page => $_[0]) };
+ $results{continuation} = sub { get_results_from_list($results, %args, page => ($args{page} + 1)) };
+ #$results{prevPageToken} = sub { get_results_from_list($results, %args, page => (($args{page} > 1) ? ($args{page} - 1) : do { warn_first_page(); 1 })) };
+#>>>
+
+ scalar {results => \%results, url => undef};
+}
+
+sub print_local_playlist {
+ my ($name) = @_;
+
+ $name //= '';
+
+ require File::Basename;
+
+ my @playlist_files = reverse $yv_utils->get_local_playlist_filenames($local_playlists_dir);
+ my $regex = qr/\Q$name\E/i;
+
+ if ($name eq '') {
+ my $results = get_results_from_list(
+ \@playlist_files,
+ callback => sub {
+ my ($id) = @_;
+ $yv_utils->local_playlist_snippet($id);
+ }
+ );
+ return print_playlists($results);
+ }
+
+ foreach my $file (@playlist_files) {
+ if (File::Basename::basename($file) =~ $regex or $file eq $name) {
+ return print_videos_from_data_file($file);
+ }
+ }
+
+ warn_no_thing_selected('playlist');
+ return 0;
+}
+
+sub print_videos_from_data_file {
+ my ($file) = @_;
+ my $videos = eval { Storable::retrieve($file) } // [];
+ print_videos(get_results_from_list($videos));
+}
+
+sub print_watched_videos {
+ print_videos_from_data_file($watch_history_data_file);
+}
+
+sub print_liked_videos {
+ print_videos_from_data_file($liked_videos_data_file);
+}
+
+sub print_disliked_videos {
+ print_videos_from_data_file($disliked_videos_data_file);
+}
+
+sub print_favorite_videos {
+ print_videos_from_data_file($favorite_videos_data_file);
+}
+
+sub print_subscription_videos {
+ print_videos_from_data_file($subscription_videos_data_file);
+}
+
+sub print_local_subscription_videos {
+
+ # Reuse the subscription file if it's less than 10 minutes old
+ if (-f $subscription_videos_data_file and (-M _) < (1 / 6) / 24 and (-M _) < ((-M $opt{subscribed_channels_file}) // 0)) {
+ return print_subscription_videos();
+ }
+
+ my @channels = $yv_utils->read_channels_from_file($opt{subscribed_channels_file});
+
+ if (not @channels) {
+ warn "\n[!] No subscribed channels...\n";
+ return;
+ }
+
+ my %subscriptions;
+
+ print "\n" if @channels;
+
+ require Time::Piece;
+ my $time = Time::Piece->new();
+
+ my @items;
+ foreach my $i (0 .. $#channels) {
+
+ local $| = 1;
+ printf("[%d/%d] Retrieving info for $channels[$i][1]...\r", $i + 1, $#channels + 1);
+
+ my $id = $channels[$i][0] // next;
+ my $uploads = $yv_obj->uploads($id) // next;
+
+ my $videos = $uploads->{results} // [];
+
+ if (ref($videos) eq 'HASH' and exists $videos->{videos}) {
+ $videos = $videos->{videos};
+ }
+
+ if (ref($videos) eq 'HASH' and exists $videos->{entries}) {
+ $videos = $videos->{entries};
+ }
+
+ if (ref($videos) ne 'ARRAY') {
+ next;
+ }
+
+ $subscriptions{$id} = 1;
+ $subscriptions{lc($id)} = 1;
+
+ foreach my $video (@$videos) {
+ $video->{timestamp} = [@$time];
+ }
+
+ push @items, @$videos;
+ }
+
+ print "\n" if @items;
+
+ my $subscriptions_data = [];
+
+ if (-f $subscription_videos_data_file) {
+ $subscriptions_data = eval { Storable::retrieve($subscription_videos_data_file) } // [];
+ }
+
+ unshift(@$subscriptions_data, @items);
+
+ # Remove duplicates
+ @$subscriptions_data = do {
+ my %seen;
+ grep { !$seen{$yv_utils->get_video_id($_)}++ } @$subscriptions_data;
+ };
+
+ # Remove videos from unsubscribed channels
+ @$subscriptions_data = grep {
+ exists($subscriptions{$yv_utils->get_channel_id($_)})
+ or exists($subscriptions{lc($yv_utils->get_channel_title($_) // '')})
+ } @$subscriptions_data;
+
+ # Order videos by newest first
+ @$subscriptions_data =
+ map { $_->[0] }
+ sort { $b->[1] <=> $a->[1] }
+ map { [$_, $yv_utils->get_publication_time($_)] } @$subscriptions_data;
+
+ # Remove results from the end when the list becomes too large
+ my $subscriptions_limit = $opt{subscriptions_limit} // 1e4;
+ if ($subscriptions_limit > 0 and scalar(@$subscriptions_data) > $subscriptions_limit) {
+ $#$subscriptions_data = $subscriptions_limit;
+ }
+
+ if (@$subscriptions_data) {
+ Storable::store($subscriptions_data, $subscription_videos_data_file);
+ }
+
+ print_videos(get_results_from_list($subscriptions_data));
+}
+
+sub _print_local_channel_from_file {
+ my ($name, $file) = @_;
+
+ $name //= '';
+
+ my @users = $yv_utils->read_channels_from_file($file);
+ my $regex = qr/\Q$name\E/i;
+
+ if ($name eq '') {
+
+ my $results = get_results_from_list(
+ \@users,
+ callback => sub {
+ my ($entry) = @_;
+ my ($id, $name) = @$entry;
+ $yv_utils->local_channel_snippet($id, $name);
+ }
+ );
+
+ return print_channels($results);
+ }
+
+ foreach my $user (@users) {
+ my ($channel_id, $channel_name) = @$user;
+
+ if ($channel_id eq $name or $channel_name =~ $regex) {
+ return print_videos($yv_obj->uploads($channel_id));
+ }
+ }
+
+ warn_no_thing_selected('channel');
+ return 0;
+}
+
+sub print_saved_channels {
+ my ($name) = @_;
+ _print_local_channel_from_file($name, $opt{youtube_users_file});
+}
+
+sub print_subscribed_channels {
+ my ($name) = @_;
+ _print_local_channel_from_file($name, $opt{subscribed_channels_file});
+}
+
sub print_categories {
my ($results) = @_;
@@ -2753,7 +3197,7 @@ sub print_playlists {
warn_no_results("playlist");
}
- if ($opt{get_term_width} and $opt{results_fixed_width}) {
+ if ($opt{get_term_width}) {
get_term_width();
}
@@ -2764,77 +3208,27 @@ sub print_playlists {
if (exists $playlists->{playlists}) {
$playlists = $playlists->{playlists};
}
+ elsif (exists $playlists->{entries}) {
+ $playlists = $playlists->{entries};
+ }
else {
warn "\n[!] No playlists...\n";
$playlists = [];
}
}
- state $info_format = <<"FORMAT";
-
-TITLE: %s
- ID: %s
- URL: https://www.youtube.com/playlist?list=%s
-DESCR: %s
-FORMAT
+ my @formatted;
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 $entry = $opt{custom_playlist_layout_format};
- my $playlist = $playlists->[$i];
- my $title = clear_title($yv_utils->get_title($playlist));
+ push @formatted, format_line_result($i, $entry, $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)
- );
- }
+ if (@formatted) {
+ print "\n" . join("", @formatted);
}
state @keywords;
@@ -2873,17 +3267,12 @@ FORMAT
elsif ($opt =~ /^(?:r|return)\z/) {
return;
}
- elsif ($opt =~ /^i(?:nfo)?${digit_or_equal_re}(.*)/) {
+
+ # :i=i, :info=i
+ elsif ($opt =~ /^(?:i|info)${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;
+ print_playlist_info($playlists->[$id]);
}
press_enter_to_continue();
}
@@ -2891,6 +3280,8 @@ FORMAT
warn_no_thing_selected('playlist');
}
}
+
+ # :pp=i
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]);
@@ -2915,7 +3306,12 @@ FORMAT
return $id;
}
- get_and_print_videos_from_playlist($id);
+ if ($id =~ m{^/}) { # local playlist
+ print_local_playlist($id);
+ }
+ else {
+ get_and_print_videos_from_playlist($id);
+ }
}
else {
push @for_search, $key;
@@ -2980,28 +3376,35 @@ sub get_streaming_url {
my $srt_file;
if (ref($captions) eq 'ARRAY' and @$captions and $opt{get_captions} and not $opt{novideo}) {
require WWW::FairViewer::GetCaption;
+
+ my $languages = $opt{srt_languages};
+
+ if (ref($languages) ne 'ARRAY') {
+ $languages = [grep { /[a-z]/i } split(/\s*,\s*/, $languages)];
+ }
+
my $yv_cap = WWW::FairViewer::GetCaption->new(
- auto_captions => $opt{auto_captions},
- captions_dir => $opt{cache_dir},
- captions => $captions,
- languages => $CONFIG{srt_languages},
- yv_obj => $yv_obj,
- );
+ auto_captions => $opt{auto_captions},
+ captions_dir => $opt{cache_dir},
+ captions => $captions,
+ languages => $languages,
+ yv_obj => $yv_obj,
+ );
$srt_file = $yv_cap->save_caption($video_id);
}
require WWW::FairViewer::Itags;
state $yv_itags = WWW::FairViewer::Itags->new();
- # Include DASH itags
- my $dash = 1;
+ # Include split-videos
+ my $split_videos = 1;
- # Exclude DASH itags in download-mode or when no video output is required
- if ($opt{novideo} or not $opt{dash_support}) {
- $dash = 0;
+ # Exclude split-videos in download-mode or when no video output is required
+ if ($opt{novideo} or not $opt{split_videos}) {
+ $split_videos = 0;
}
elsif ($opt{download_video}) {
- $dash = $opt{merge_into_mkv} ? 1 : 0;
+ $split_videos = $opt{merge_into_mkv} ? 1 : 0;
}
my ($streaming, $resolution) = $yv_itags->find_streaming_url(
@@ -3011,9 +3414,11 @@ sub get_streaming_url {
hfr => $opt{hfr},
ignore_av1 => $opt{ignore_av1},
- dash => $dash,
- dash_mp4_audio => ($opt{novideo} ? 1 : $opt{dash_mp4_audio}),
- dash_segmented => ($opt{download_video} ? 0 : $opt{dash_segmented}),
+ split => $split_videos,
+ prefer_m4a => $opt{prefer_m4a},
+ dash => ($opt{download_video} ? 0 : $opt{dash}),
+
+ ignored_projections => $opt{ignored_projections},
);
return {
@@ -3042,7 +3447,12 @@ sub download_from_url {
if (defined($lwp_dl)) {
my @cmd = ($lwp_dl, $url, "$output_filename.part");
$yv_obj->proxy_system(@cmd);
- return if $?;
+ if ($? == 256 and !defined(fileno(STDOUT))) { # lwp-download bug
+ ## ok
+ }
+ else {
+ return if $?;
+ }
rename("$output_filename.part", $output_filename) or return undef;
return $output_filename;
}
@@ -3083,19 +3493,12 @@ sub download_from_url {
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,
+ fat32safe => $opt{fat32safe},
);
my $naked_filename = $video_filename =~ s/\.\w+\z//r;
@@ -3199,6 +3602,7 @@ sub download_video {
# Convert the downloaded video
if (defined $opt{convert_to}) {
+
my $convert_filename = catfile($opt{downloads_dir}, "$naked_filename.$opt{convert_to}");
my $convert_cmd = $opt{convert_cmd};
@@ -3263,17 +3667,45 @@ sub download_video {
return 1;
}
+sub prepend_video_data_to_file {
+ my ($video_data, $file) = @_;
+
+ my $videos = eval { Storable::retrieve($file) } // [];
+
+ if (ref($video_data) ne 'HASH') {
+ my $videoID = get_valid_video_id($video_data) // return;
+ $video_data = $yv_obj->video_details($videoID);
+ }
+
+ get_valid_video_id($yv_utils->get_video_id($video_data)) // return;
+
+ unshift(@$videos, $video_data);
+
+ my %seen;
+ @$videos = grep { !$seen{$yv_utils->get_video_id($_)}++ } @$videos;
+
+ Storable::store($videos, $file);
+ return 1;
+}
+
sub save_watched_video {
- my ($video_id) = @_;
+ my ($video_id, $video_data) = @_;
+
+ if ($opt{watch_history}) {
- 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;
+ if (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;
+ }
+
+ prepend_video_data_to_file($video_data, $watch_history_data_file);
}
- $watched_videos{$video_id} = 1;
+ $WATCHED_VIDEOS{$video_id} = 1;
return 1;
}
@@ -3309,7 +3741,7 @@ sub get_player_command {
)
);
- my $has_video = $cmd =~ /\*(?:VIDEO|URL|ID)\*/;
+ my $has_video = $cmd =~ /(?:^|\s)\*(?:VIDEO|URL)\*(?:\s|\z)/;
$cmd = $yv_utils->format_text(
streaming => $streaming,
@@ -3355,7 +3787,7 @@ sub play_videos {
}
# Ignore already watched videos
- if (exists($watched_videos{$video_id}) and $opt{skip_watched}) {
+ if (exists($WATCHED_VIDEOS{$video_id}) and $opt{skip_watched}) {
say ":: Already watched video (ID: $video_id)... Skipping...";
next;
}
@@ -3440,7 +3872,7 @@ sub play_videos {
}
}
- save_watched_video($video_id);
+ save_watched_video($video_id, $video);
press_enter_to_continue() if $opt{confirm};
}
@@ -3476,11 +3908,113 @@ sub play_videos_matched_by_regex {
return 1;
}
+sub print_playlist_info {
+ my ($playlist) = @_;
+
+ my $hr = '-' x ($opt{get_term_width} ? get_term_width() : $term_width);
+
+ printf(
+ "\n%s\n%s\n%s\n",
+ _bold_color('=> Description'),
+ $hr,
+ wrap_text(
+ i_tab => q{},
+ s_tab => q{},
+ text => [$yv_utils->get_description($playlist) || 'No description available...']
+ ),
+ );
+
+ my $id = $yv_utils->get_playlist_id($playlist);
+
+ if ($id =~ m{^/}) {
+ ## local playlist
+ }
+ else {
+ say STDOUT $hr, "\n", _bold_color('=> URL: '), sprintf("https://www.youtube.com/playlist?list=%s", $id);
+ }
+
+ my $title = $yv_utils->get_title($playlist);
+ my $title_length = length($title);
+ my $rep = ($term_width - $title_length) / 2 - 4;
+
+ $rep = 0 if $rep < 0;
+
+ print(
+ "$hr\n",
+ q{ } x $rep => (_bold_color("=>> $title <<=") . "\n\n"),
+ (
+ map { sprintf(q{-> } . "%-*s: %s\n", $opt{_colors} ? 18 : 10, _bold_color($_->[0]), $_->[1]) }
+ grep { defined($_->[1]) } (
+ ['Title' => $yv_utils->get_title($playlist)],
+ ['Author' => $yv_utils->get_channel_title($playlist)],
+ ['ChannelID' => $yv_utils->get_channel_id($playlist)],
+ ['PlaylistID' => ($id =~ m{^/} ? undef : $id)],
+ ['Videos' => $yv_utils->set_thousands($yv_utils->get_playlist_item_count($playlist))],
+ ['Published' => $yv_utils->get_publication_date($playlist)],
+ )
+ ),
+ "$hr\n"
+ );
+
+ return 1;
+}
+
+sub print_channel_info {
+ my ($channel) = @_;
+
+ 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($channel) || 'No description available...']
+ ),
+ $hr,
+ _bold_color('=> URL: ')
+ );
+
+ print STDOUT sprintf("https://www.youtube.com/channel/%s", $yv_utils->get_channel_id($channel));
+
+ my $title = $yv_utils->get_channel_title($channel);
+ 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} ? 20 : 12, _bold_color($_->[0]), $_->[1]) }
+ grep { defined($_->[1]) } (
+ ['Channel' => $title],
+ ['ChannelID' => $yv_utils->get_channel_id($channel)],
+ ['Videos' => $yv_utils->set_thousands($yv_utils->get_channel_video_count($channel))],
+ ['Subscribers' => $yv_utils->set_thousands($yv_utils->get_channel_subscriber_count($channel))],
+ ['Published' => $yv_utils->get_publication_date($channel)],
+ )
+ ),
+ "$hr\n"
+ );
+
+ return 1;
+}
+
sub print_video_info {
my ($video) = @_;
$opt{show_video_info} || return 1;
+ my $extra_info = $yv_obj->video_details($yv_utils->get_video_id($video) // return 1);
+
+ foreach my $key (keys %$extra_info) {
+ $video->{$key} = $extra_info->{$key};
+ }
+
my $hr = '-' x ($opt{get_term_width} ? get_term_width() : $term_width);
printf(
@@ -3514,6 +4048,7 @@ sub print_video_info {
['ChannelID' => $yv_utils->get_channel_id($video)],
['VideoID' => $yv_utils->get_video_id($video)],
['Category' => $yv_utils->get_category_name($video)],
+ ['Rating' => $yv_utils->get_rating($video)],
['Definition' => $yv_utils->get_definition($video)],
['Duration' => $yv_utils->get_time($video)],
['Likes' => $yv_utils->set_thousands($yv_utils->get_likes($video))],
@@ -3535,7 +4070,7 @@ sub print_videos {
warn_no_results("video");
}
- if ($opt{get_term_width} and $opt{results_fixed_width}) {
+ if ($opt{get_term_width}) {
get_term_width();
}
@@ -3546,13 +4081,20 @@ sub print_videos {
$videos = $videos->{videos};
}
- if (ref($videos) ne 'ARRAY') {
+ if (ref($videos) eq 'HASH' and exists $videos->{entries}) {
+ $videos = $videos->{entries};
+ }
- my $current_instance = $yv_obj->get_api_host();
+ my $token = undef;
- say "\n:: Probably $current_instance is down. Try:";
+ if (ref($results->{results}) eq 'HASH' and exists $results->{results}{continuation}) {
+ $token = $results->{results}{continuation};
+ }
+
+ if (ref($videos) ne 'ARRAY') {
+ say "\n:: Probably the selected invidious instance is down. Try:";
say "\n\t$0 --api=auto\n";
- say "See also: https://libregit.org/heckyel/fair-viewer#invidious-instances";
+ say "See also: https://git.sr.ht/~heckyel/fair-viewer#invidious-instances";
return;
}
@@ -3619,130 +4161,17 @@ sub print_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;
- }
+ my $video = $videos->[$i];
+ my $entry = $opt{custom_layout_format};
- 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),
- );
- }
+ push @formatted, format_line_result($i, $entry, $video);
}
if ($opt{highlight_watched}) {
foreach my $i (0 .. $#{$videos}) {
my $video = $videos->[$i];
- if (exists($watched_videos{$yv_utils->get_video_id($video)})) {
+ if (exists($WATCHED_VIDEOS{$yv_utils->get_video_id($video)})) {
$formatted[$i] = colored(colorstrip($formatted[$i]), $opt{highlight_color});
}
}
@@ -3763,7 +4192,7 @@ sub print_videos {
) {
if ($opt{play_backwards}) {
if (defined($url)) {
- __SUB__->($yv_obj->previous_page($url), auto => 1);
+ return;
}
else {
$opt{play_backwards} = 0;
@@ -3772,8 +4201,8 @@ sub print_videos {
}
}
else {
- if (defined($url)) {
- __SUB__->($yv_obj->next_page($url), auto => 1);
+ if (defined($url) or ref($token) eq 'CODE') {
+ __SUB__->($yv_obj->next_page($url, $token), auto => 1);
}
else {
$opt{play_all} = 0;
@@ -3827,8 +4256,8 @@ sub print_videos {
press_enter_to_continue();
}
elsif ($opt =~ /^(?:n|next)\z/) {
- if (defined($url)) {
- my $request = $yv_obj->next_page($url);
+ if (defined($url) or ref($token) eq 'CODE') {
+ my $request = $yv_obj->next_page($url, $token);
__SUB__->($request, @keywords ? (auto => 1) : ());
}
else {
@@ -3840,21 +4269,19 @@ sub print_videos {
}
}
}
- elsif ($opt =~ /^(?:b|back|p|prev|previous)\z/) {
- if (defined($url)) {
- __SUB__->($yv_obj->previous_page($url), @keywords ? (auto => 1) : ());
- }
- else {
- warn_first_page();
- }
- }
+
+ # :refresh
elsif ($opt =~ /^(?:R|refresh)\z/) {
@{$videos} = @{$yv_obj->_get_results($url)->{results}};
$results->{has_extra_info} = 0;
}
+
+ # :r, :return
elsif ($opt =~ /^(?:r|return)\z/) {
return;
}
+
+ # :author=i, :u=i
elsif ($opt =~ /^(?:a|author|u|uploads)${digit_or_equal_re}(.*)/) {
if (my @nums = get_valid_numbers($#{$videos}, $1)) {
foreach my $id (@nums) {
@@ -3872,49 +4299,56 @@ sub print_videos {
warn_no_thing_selected('video');
}
}
- elsif ($opt =~ /^(?:pv|popular)${digit_or_equal_re}(.*)/) {
+
+ # :s=i, :subscribe=i
+ elsif ($opt =~ /^(?:s|sub(?:scribe)?)${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');
- }
+ my $channel_id = $yv_utils->get_channel_id($videos->[$id]);
+ my $channel_title = $yv_utils->get_channel_title($videos->[$id]);
+ subscribe_channel($channel_id, $channel_title);
}
}
else {
warn_no_thing_selected('video');
}
}
- elsif ($opt =~ /^(?:A|[Aa]ctivity)${digit_or_equal_re}(.*)/) {
+
+ # :save=i
+ elsif ($opt =~ /^(?:save)${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 $channel_title = $yv_utils->get_channel_title($videos->[$id]);
+ save_channel($channel_id, $channel_title);
+ }
+ }
+ else {
+ warn_no_thing_selected('video');
+ }
+ }
+
+ # :popular=i, :pv=i
+ 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->activities($channel_id);
+ my $request = $yv_obj->popular_videos($channel_id);
if ($yv_utils->has_entries($request)) {
__SUB__->($request);
}
else {
- warn_no_results('activity');
+ warn_no_results('popular video');
}
}
}
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}(.*)/) {
+
+ # :p=i, :playlist=i, :up=i
+ elsif ($opt =~ /^(?:p|l|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]));
@@ -3930,10 +4364,12 @@ sub print_videos {
warn_no_thing_selected('video');
}
}
+
+ # :like=i, :dislike=i
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);
+ rate_videos($rating, map { $videos->[$_] } @nums);
}
else {
warn_no_thing_selected('video');
@@ -3941,15 +4377,7 @@ sub print_videos {
}
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);
+ favorite_videos(map { $videos->[$_] } @nums);
}
else {
warn_no_thing_selected('video');
@@ -4150,7 +4578,11 @@ main_quit(0);
=head2 api_host
-Hostname of an invidious instance. When set to C<"auto">, a random invidious is selected everytime when the program is started.
+Hostname of an invidious instance. When set to C<"auto">, a random invidious is selected on-demand.
+
+List of public invidious instances:
+
+ https://api.invidious.io/
=head2 auto_captions
@@ -4200,7 +4632,7 @@ The file must be a C<# Netscape HTTP Cookie File>. Same format as C<hypervideo>
See also:
- https://libregit.org/heckyel/hypervideo#how-do-i-pass-cookies-to-hypervideo
+ https://git.conocimientoslibres.ga/software/hypervideo.git/about/#how-do-i-pass-cookies-to-hypervideo
=head2 copy_caption
@@ -4208,18 +4640,12 @@ When downloading a video, copy the closed-caption (if any) in the same folder wi
If C<merge_into_mkv> and C<merge_with_captions> are both enabled, there is no need to enable this option.
-=head2 custom_layout
-
-Use a custom layout for video results, defined in C<custom_layout_format>.
-
-Requires: L<Unicode::GCString> or L<Text::CharWidth>.
-
=head2 custom_layout_format
An array of hash values specifying a custom layout for video results.
align # "left" or "right"
- color # any color supported by Term::ANSIColor
+ color # any color name supported by Term::ANSIColor
text # the actual text
width # width allocated for the text
@@ -4229,17 +4655,19 @@ The special tokens for C<text> are listed in:
fair-viewer --tricks
-=head2 dash_mp4_audio
+For better formatting, it's highly recommended to install L<Unicode::GCString> or L<Text::CharWidth>.
-Include or exclude MP4/M4A (AAC) audio files.
+=head2 custom_channel_layout_format
-=head2 dash_segmented
+An array of hash values specifying a custom layout for channel results.
-Include or exclude streams in "Dynamic Adaptive Streaming over HTTP" (DASH) format.
+=head2 custom_playlist_layout_format
-=head2 dash_support
+An array of hash values specifying a custom layout for playlist results.
-Enable or disable support for split videos.
+=head2 dash
+
+Include or exclude streams in "Dynamic Adaptive Streaming over HTTP" (DASH) format.
=head2 date
@@ -4291,13 +4719,15 @@ Read the terminal width (`stty size`).
=head2 hfr
-Include or exclude High Frame Rate (HFR) videos.
+Prefer or ignore High Frame Rate (HFR) video streams.
+
+Try to disable this option if the videos are lagging or dropping frames.
=head2 highlight_color
Highlight color used to highlight watched videos.
-Any color supported by L<Term::ANSIColor> can be used.
+Any color name supported by L<Term::ANSIColor> can be used.
=head2 highlight_watched
@@ -4317,7 +4747,7 @@ File where to save the input history.
Maximum number of entries in the history file.
-When the limit is reached, the first half of the history file will be deleted.
+When the limit is reached, the oldest half of the history file will be deleted.
Set the value to C<-1> for no limit.
@@ -4325,16 +4755,24 @@ Set the value to C<-1> for no limit.
Set HTTP(S)/SOCKS proxy, using the format:
- proto://domain.tld:port/
+ 'proto://domain.tld:port/'
If authentication is required, use:
- proto://user:pass@domain.tld:port/
+ 'proto://user:pass@domain.tld:port/'
=head2 ignore_av1
Ignore videos in AV1 format.
+=head2 ignored_projections
+
+An array of video projects to ignore.
+
+For example, to prefer rectangular projections of 360° videos, use:
+
+ ignored_projections => ["mesh", "equirectangular"],
+
=head2 interactive
Interactive mode, prompting for user-input.
@@ -4351,7 +4789,7 @@ Currently, this is not implemented.
=head2 merge_into_mkv
-During download, merge the audio+video files into an MKV container.
+When downloading split videos, merge the audio+video files into an MKV container.
Requires C<ffmpeg>.
@@ -4375,17 +4813,29 @@ Page number of results.
=head2 prefer_av1
-Prefer videos in AV1 format. (just for testing)
+Prefer videos in AV1 format. (experimental)
=head2 prefer_mp4
Prefer videos in MP4 (AVC) format.
+Try to enable this option if the videos are lagging or dropping frames.
+
+=head2 prefer_m4a
+
+Prefer audio streams in M4A (AAC) format.
+
+By default, the OPUS format for audio is preferred.
+
+=head2 prefer_invidious
+
+Prefer invidious instances over parsing the YouTube website directly.
+
=head2 region
ISO 3166 country code (default: "US").
-=head2 remember_watched
+=head2 watch_history
Set to C<1> to remember and highlight watched videos across multiple sessions.
@@ -4401,20 +4851,6 @@ Preferred resolution for videos.
Valid values: best, 2160p, 1440p, 1080p, 720p, 480p, 360p, 240p, 144p, audio.
-=head2 results_fixed_width
-
-Results in fixed-width format.
-
-Requires: L<Unicode::GCString> or L<Text::CharWidth>.
-
-=head2 results_with_colors
-
-Results with colors.
-
-=head2 results_with_details
-
-Results with extra details.
-
=head2 show_video_info
Show extra info for videos when selected.
@@ -4427,13 +4863,21 @@ When downloading, skip if the file already exists locally.
Skip already watched/downloaded videos.
+=head2 split_videos
+
+Enable or disable support for split-videos. Split-videos are videos that do not include audio and video in the same file.
+
=head2 srt_languages
List of SRT languages in the order of preference.
-=head2 subscriptions_order
+=head2 subscribed_channels_file
+
+Absolute path to the file where to store subscribed channels (C<:sub=i>).
+
+=head2 subscriptions_limit
-Order of subscriptions. Currently, not implemented.
+Maximum number of subscription videos to store in the local database. Set to C<0> for no limit.
=head2 thousand_separator
@@ -4457,7 +4901,7 @@ The available special tokens are listed in:
=head2 video_player_selected
-The selected video player defined the C<video_players> table.
+The selected video player defined in the C<video_players> table.
=head2 video_players
@@ -4474,7 +4918,7 @@ The keys for each player are:
=head2 videoCaption
-When set to C<1> or C<"true">, retrieve only the videos that contain closed-captions in search results.
+When set to C<1> or C<"true">, retrieve only videos that contain closed-captions in search results.
=head2 videoDefinition
@@ -4494,14 +4938,24 @@ Valid values: "any", "short", "long".
When set to C<"creative_commons">, retrieve only videos under the I<Creative Commons> license in search results.
+=head2 watch_history
+
+Remember watched videos. Watched videos can be listed with:
+
+ fair-viewer -wv
+
=head2 watched_file
-File where to save the video IDs of watched/downloaded videos when C<remember_watched> is set to a true value.
+File where to save the video IDs of watched/downloaded videos when C<watch_history> is set to a true value.
=head2 wget_cmd
Command for C<wget> when C<download_with_wget> is set to a true value.
+=head2 youtube_users_file
+
+Absolute path to the file where to store saved channels (C<:save=i>).
+
=head2 youtube_video_url
Format for C<sprintf()> for constructing an YouTube video URL given the video ID.
@@ -4529,11 +4983,11 @@ https://github.com/iv-org/invidious/wiki/API
=head1 REPOSITORY
-https://libregit.org/heckyel/fair-viewer
+https://git.sr.ht/~heckyel/fair-viewer
=head1 LICENSE AND COPYRIGHT
-Copyright 2010-2020 Trizen.
+Copyright 2010-2021 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