aboutsummaryrefslogtreecommitdiffstats
path: root/bin
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
parentc1322a4e9a1fb0a286dab1277a740072d0ab30f9 (diff)
downloadfair-viewer-739c821a54c01816e60eb5f774c8977a1e221ea0.tar.lz
fair-viewer-739c821a54c01816e60eb5f774c8977a1e221ea0.tar.xz
fair-viewer-739c821a54c01816e60eb5f774c8977a1e221ea0.zip
upstream
Diffstat (limited to 'bin')
-rwxr-xr-xbin/fair-viewer2014
-rwxr-xr-xbin/gtk-fair-viewer1509
2 files changed, 2230 insertions, 1293 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
diff --git a/bin/gtk-fair-viewer b/bin/gtk-fair-viewer
index 62a6d18..3cba73a 100755
--- a/bin/gtk-fair-viewer
+++ b/bin/gtk-fair-viewer
@@ -1,7 +1,7 @@
#!/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 +15,9 @@
#
#-------------------------------------------------------
# GTK Fair Viewer
-# Fork: 14 February 2020
-# Edit: 30 October 2020
-# https://framagit.org/heckyel/fair-viewer
+# Fork: 30 October 2020
+# Edit: 19 June 2021
+# https://git.sr.ht/~heckyel/fair-viewer
#-------------------------------------------------------
# This is a fork of youtube-viewer:
@@ -29,8 +29,8 @@ use 5.016;
use warnings;
no warnings 'once';
-my $DEVEL; # true in devel mode
-use if ($DEVEL = 0), lib => qw(../lib); # devel only
+my $DEVEL; # true in devel mode
+use if ($DEVEL = -w __FILE__), lib => qw(../lib); # devel only
use WWW::FairViewer v1.0.6;
use WWW::FairViewer::RegularExpressions;
@@ -45,13 +45,20 @@ use File::Spec::Functions qw(
path
tmpdir
file_name_is_absolute
- );
+);
+require Storable;
binmode(STDOUT, ':utf8');
my $appname = 'GTK+ Fair Viewer';
my $version = $WWW::FairViewer::VERSION;
+# Saved and subscribed channels
+my %channels;
+my %subscribed_channels;
+my %removed_channels;
+my %unsubbed_channels;
+
# Share directory
my $share_dir =
($DEVEL and -d "../share")
@@ -83,27 +90,47 @@ else {
$xdg_config_home = catdir($home_dir, '.config');
}
-# Configuration dir/file
+# Configuration dirs
my $config_dir = catdir($xdg_config_home, 'fair-viewer');
-my $config_file = catfile($config_dir, "gtk-fair-viewer.conf");
-my $youtube_users_file = catfile($config_dir, 'users.txt');
-my $history_file = catfile($config_dir, 'gtk-history.txt');
-my $session_file = catfile($config_dir, 'session.dat');
-my $authentication_file = catfile($config_dir, 'reg.dat');
-my $api_file = catfile($config_dir, 'api.json');
+my $local_playlists_dir = catdir($config_dir, 'playlists');
+
+# Config files
+my $config_file = catfile($config_dir, "gtk-fair-viewer.conf");
+my $youtube_users_file = catfile($config_dir, 'users.txt');
+my $subscribed_channels_file = catfile($config_dir, 'subscribed_channels.txt');
+my $history_file = catfile($config_dir, 'gtk-history.txt');
+my $session_file = catfile($config_dir, 'session.dat');
+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 configuration directory
-foreach my $dir ($config_dir) {
+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 the configuration directory `$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);
}
}
# Video queue for the enqueue feature
my @VIDEO_QUEUE;
+# Keep track of watched videos
+my %WATCHED_VIDEOS;
+
sub which_command {
my ($cmd) = @_;
@@ -121,19 +148,21 @@ sub which_command {
}
my %symbols = (
- up_arrow => '↑',
- down_arrow => '↓',
- diamond => '❖',
- face => '☺',
- black_face => '☻',
- average => 'x̄',
- ellipsis => '…',
- play => '▶',
- views => '◈',
- heart => '❤',
- right_arrow => '→',
- crazy_arrow => '↬',
- numero => '№',
+ thumbs_up => '👍',
+ thumbs_down => '👎',
+ type => '💡',
+ author => '😃',
+ author_id => '🤖',
+ average => '📊',
+ category => '🗃️',
+ play => '▶️',
+ views => '👀',
+ heart => '❤️',
+ published => '⏱️',
+ updated => '✨',
+ numero => '#️⃣',
+ video => '🎞️',
+ subs => '👪',
);
# Main configuration
@@ -162,7 +191,7 @@ my %CONFIG = (
arg => q{--really-quiet --force-media-title=*TITLE* --no-ytdl *VIDEO*},
},
},
- video_player_selected => 'mpv', # autodetect it later
+ video_player_selected => undef, # autodetect it later
# GUI options
clear_text_entries_on_click => 0,
@@ -176,22 +205,23 @@ my %CONFIG = (
hpaned_width => 250,
hpaned_position => 420,
- # Fair options
- dash_support => 1,
- dash_mp4_audio => 1,
- dash_segmented => 1, # may load slow
- prefer_mp4 => 0,
- prefer_av1 => 0,
- ignore_av1 => 0,
- maxResults => 10,
- hfr => 1, # true to prefer high frame rate (HFR) videos
- resolution => 'best',
- videoDimension => undef,
- videoLicense => undef,
- region => undef,
-
- comments_width => 80, # wrap comments longer than `n` characters
- comments_order => 'top', # valid values: time, relevance
+ # Pipe options
+ split_videos => 1,
+ dash => 1, # may load slow
+ prefer_mp4 => 0,
+ prefer_av1 => 0,
+ ignore_av1 => 0,
+ prefer_m4a => 0,
+ prefer_invidious => 0,
+ maxResults => 10,
+ hfr => 1, # true to prefer high frame rate (HFR) videos
+ resolution => 'best',
+ videoDimension => undef,
+ videoLicense => undef,
+ region => undef,
+
+ comments_width => 80, # wrap comments longer than `n` characters
+ comments_order => 'top', # valid values: time, relevance
# API
api_host => "auto",
@@ -206,7 +236,7 @@ my %CONFIG = (
srt_languages => ['en', 'es'],
get_captions => 1,
auto_captions => 0,
- cache_dir => undef,
+ cache_dir => undef, # will be defined later
# Others
env_proxy => 1,
@@ -228,22 +258,36 @@ my %CONFIG = (
tooltips => 1,
tooltip_max_len => 512, # max length of description in tooltips
- thousand_separator => q{,},
- downloads_dir => curdir(),
- web_browser => undef, # defaults to $ENV{WEBBROWSER} or xdg-open
- terminal => undef, # autodetect it later
- terminal_exec => q{-e '%s'},
- fair_viewer => undef,
- fair_viewer_args => [],
- youtube_users_file => $youtube_users_file,
+ thousand_separator => q{,},
+ downloads_dir => curdir(),
+ web_browser => undef, # defaults to $ENV{WEBBROWSER} or xdg-open
+ terminal => undef, # autodetect it later
+ terminal_exec => q{-e '%s'},
+ pipe_viewer => undef,
+ pipe_viewer_args => [],
+
+ ignored_projections => [],
+
+ # 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,
+
history => 1,
history_limit => 100_000,
history_file => $history_file,
recent_history => 10,
remember_session => 1,
remember_session_depth => 10,
- save_titles_to_history => 0,
entry_completion_limit => 10,
+
+ # Save titles
+ save_titles_to_history => 0,
+ save_watched_to_history => 0,
);
{
@@ -353,7 +397,6 @@ my %objects = (
'clear_list_checkbutton' => \my $clear_list_checkbutton,
'dash_checkbutton' => \my $dash_checkbutton,
'audio_only_checkbutton' => \my $audio_only_checkbutton,
- 'gif_spinner' => \my $gif_spinner,
'hbox2' => \my $hbox2,
'feeds_title' => \my $feeds_title,
'channel_name_save' => \my $save_channel_name_entry,
@@ -422,11 +465,10 @@ local $SIG{__DIE__} = sub {
#---------------------- LOAD IMAGES ----------------------#
my $app_icon_pixbuf = 'Gtk3::Gdk::Pixbuf'->new_from_file(catfile($icons_path, "gtk-fair-viewer.png"));
-my $user_icon_pixbuf = 'Gtk3::Gdk::Pixbuf'->new_from_file_at_size(catfile($icons_path, "user.png"), 16, 16);
-my $feed_icon_pixbuf = 'Gtk3::Gdk::Pixbuf'->new_from_file_at_size(catfile($icons_path, "feed.png"), 16, 16);
-my $feed_icon_gray_pixbuf = 'Gtk3::Gdk::Pixbuf'->new_from_file_at_size(catfile($icons_path, "feed_gray.png"), 16, 16);
+my $user_icon_pixbuf = 'Gtk3::Gdk::Pixbuf'->new_from_file_at_size(catfile($icons_path, "user.png"), 16, 16);
+my $feed_icon_pixbuf = 'Gtk3::Gdk::Pixbuf'->new_from_file_at_size(catfile($icons_path, "feed.png"), 16, 16);
+my $feed_icon_gray_pixbuf = 'Gtk3::Gdk::Pixbuf'->new_from_file_at_size(catfile($icons_path, "feed_gray.png"), 16, 16);
my $default_thumb = 'Gtk3::Gdk::Pixbuf'->new_from_file_at_size(catfile($icons_path, "default_thumb.jpg"), 160, 90);
-my $animation = 'Gtk3::Gdk::PixbufAnimation'->new_from_file(catfile($icons_path, "spinner.gif"));
# Setting application title and icon
$mainw->set_title("$appname $version");
@@ -458,6 +500,7 @@ if (not defined $CONFIG{cache_dir}) {
$CONFIG{cache_dir} = catdir($cache_dir, 'fair-viewer');
}
+# Create the cache directory (if needed)
foreach my $path ($CONFIG{cache_dir}) {
next if -d $path;
require File::Path;
@@ -550,10 +593,12 @@ foreach my $path ($CONFIG{cache_dir}) {
my $word = $words[$i];
for (my $j = $i ; $j <= $end_p ; ++$j) {
- my $part = $parts[$j];
my $matched;
my $continue = 1;
+
+ my $part = $parts[$j];
+
while ($part eq $word) {
$order_score += 1 - 1 / (length($word) + 1)**2;
$matched ||= 1;
@@ -562,8 +607,9 @@ foreach my $path ($CONFIG{cache_dir}) {
}
if ($matched) {
- $order_score += 1 - 1 / (length($word) + 1)
- if ($continue and index($part, $word) == 0);
+ if ($continue and index($part, $word) == 0) {
+ $order_score += 1 - 1 / (length($word) + 1);
+ }
last;
}
elsif (index($part, $word) == 0) {
@@ -687,8 +733,8 @@ foreach my $path ($CONFIG{cache_dir}) {
search();
}
);
- $item->set_property(tooltip_text => $text);
- $item->set_image('Gtk3::Image'->new_from_icon_name("history-view", q{menu}));
+ $item->set_property(tooltip_text => "Search for „${text}”");
+ $item->set_image('Gtk3::Image'->new_from_icon_name("system-search", q{menu}));
$item->show;
$history_menu->append($item);
}
@@ -806,19 +852,19 @@ my %ResultsHistory = (
);
# Locate CLI fair-viewer
-$CONFIG{fair_viewer} //= which_command('fair-viewer') // 'fair-viewer';
+$CONFIG{pipe_viewer} //= which_command('fair-viewer') // 'fair-viewer';
my $yv_obj = WWW::FairViewer->new(
- escape_utf8 => 1,
- config_dir => $config_dir,
- ytdl => $CONFIG{ytdl},
- ytdl_cmd => $CONFIG{ytdl_cmd},
- env_proxy => $CONFIG{env_proxy},
- cache_dir => $CONFIG{cache_dir},
- cookie_file => $CONFIG{cookie_file},
- user_agent => $CONFIG{user_agent},
- timeout => $CONFIG{timeout},
- );
+ escape_utf8 => 1,
+ config_dir => $config_dir,
+ ytdl => $CONFIG{ytdl},
+ ytdl_cmd => $CONFIG{ytdl_cmd},
+ env_proxy => $CONFIG{env_proxy},
+ cache_dir => $CONFIG{cache_dir},
+ cookie_file => $CONFIG{cookie_file},
+ user_agent => $CONFIG{user_agent},
+ timeout => $CONFIG{timeout},
+ );
#$yv_obj->load_authentication_tokens();
@@ -831,7 +877,7 @@ else {
require WWW::FairViewer::Utils;
my $yv_utils = WWW::FairViewer::Utils->new(thousand_separator => $CONFIG{thousand_separator},
- youtube_url_format => $CONFIG{youtube_video_url},);
+ youtube_url_format => $CONFIG{youtube_video_url},);
# Set default combobox values
$definition_combobox->set_active(0);
@@ -851,7 +897,7 @@ sub apply_configuration {
$audio_only_checkbutton->set_active($CONFIG{audio_only});
# DASH mode
- $dash_checkbutton->set_active($CONFIG{dash_segmented});
+ $dash_checkbutton->set_active($CONFIG{dash});
$clear_list_checkbutton->set_active($CONFIG{clear_search_list});
$panel_account_type_combobox->set_active($CONFIG{active_panel_account_combobox});
@@ -862,11 +908,11 @@ sub apply_configuration {
foreach my $option_name (
qw(
- comments_order
maxResults videoDimension videoLicense
region debug http_proxy user_agent
timeout cookie_file ytdl ytdl_cmd
api_host prefer_mp4 prefer_av1
+ comments_order prefer_invidious
)
) {
@@ -960,6 +1006,12 @@ sub set_text {
my ($object, $text, %args) = @_;
my $object_buffer = $object->get_buffer;
+ require Encode;
+
+ if (!Encode::is_utf8($text)) {
+ $text = Encode::decode_utf8($text);
+ }
+
if ($args{append}) {
my $iter = $object_buffer->get_end_iter;
$object_buffer->insert($iter, $text);
@@ -1031,21 +1083,22 @@ sub menu_popup {
# Create the main right-click menu
my $menu = 'Gtk3::Menu'->new;
+ # More details
+ {
+ my $item = 'Gtk3::ImageMenuItem'->new("Show more details");
+ $item->set_image('Gtk3::Image'->new_from_icon_name("window-new", q{menu}));
+ $item->signal_connect(activate => \&show_details_window);
+ $item->show;
+ $menu->append($item);
+ }
+
# Video menu
if ($type eq 'video') {
- my $video_id = $liststore->get($iter, 3);
-
- # More details
- {
- my $item = 'Gtk3::ImageMenuItem'->new("Show more details");
- $item->set_image('Gtk3::Image'->new_from_icon_name("window-new", q{menu}));
- $item->signal_connect(activate => \&show_details_window);
- $item->show;
- $menu->append($item);
- }
+ my $video_id = $liststore->get($iter, 3);
+ my $video_data = $yv_obj->parse_json_string($liststore->get($iter, 8));
- # Fair comments
+ # Youtube comments
{
my $item = 'Gtk3::ImageMenuItem'->new("YouTube comments");
$item->set_image('Gtk3::Image'->new_from_icon_name("edit-copy", q{menu}));
@@ -1091,11 +1144,11 @@ sub menu_popup {
# Favorite
{
my $item = 'Gtk3::ImageMenuItem'->new("Favorite");
- $item->set_property(tooltip_text => "Save the video to favorites");
+ $item->set_property(tooltip_text => "Save the video in the playlist of favorite videos");
$item->signal_connect(
activate => sub {
- $yv_obj->favorite_video($video_id)
- or warn "Failed to favorite the video <$video_id>: $!";
+ say(":: Favorite video: ", $yv_utils->get_title($video_data)) if $yv_obj->get_debug;
+ prepend_video_data_to_file($video_data, $favorite_videos_data_file);
}
);
$item->set_image('Gtk3::Image'->new_from_icon_name("starred-symbolic", q{menu}));
@@ -1123,11 +1176,11 @@ sub menu_popup {
# Like
{
my $item = 'Gtk3::ImageMenuItem'->new("Like");
- $item->set_property(tooltip_text => "Send a positive rating");
+ $item->set_property(tooltip_text => "Save video in the playlist of liked videos");
$item->signal_connect(
activate => sub {
- $yv_obj->send_rating_to_video($video_id, 'like')
- or warn "Failed to send a positive rating to <$video_id>: $!";
+ say(":: Liking video: ", $yv_utils->get_title($video_data)) if $yv_obj->get_debug;
+ prepend_video_data_to_file($video_data, $liked_videos_data_file);
}
);
$item->set_image('Gtk3::Image'->new_from_icon_name("go-up-symbolic", q{menu}));
@@ -1138,11 +1191,11 @@ sub menu_popup {
# Disike
{
my $item = 'Gtk3::ImageMenuItem'->new("Dislike");
- $item->set_property(tooltip_text => "Send a negative rating");
+ $item->set_property(tooltip_text => "Save video in the playlist of disliked videos");
$item->signal_connect(
activate => sub {
- $yv_obj->send_rating_to_video($video_id, 'dislike')
- or warn "Failed to send a negative rating to <$video_id>: $!";
+ say(":: Disliking video: ", $yv_utils->get_title($video_data)) if $yv_obj->get_debug;
+ prepend_video_data_to_file($video_data, $disliked_videos_data_file);
}
);
$item->set_image('Gtk3::Image'->new_from_icon_name("go-down-symbolic", q{menu}));
@@ -1185,7 +1238,7 @@ sub menu_popup {
my $playlist_id = $liststore->get($iter, 3);
- # More details
+ # Playlist videos
{
my $item = 'Gtk3::ImageMenuItem'->new("Videos");
$item->set_property(tooltip_text => "Display the videos from this playlist");
@@ -1232,6 +1285,16 @@ sub menu_popup {
$author->append($item);
}
+ # Playlists created by this author
+ {
+ my $item = 'Gtk3::ImageMenuItem'->new("Playlists");
+ $item->signal_connect(activate => \&show_playlists_from_selected_author);
+ $item->set_property(tooltip_text => "Show playlists created by this author");
+ $item->set_image('Gtk3::Image'->new_from_icon_name("emblem-documents-symbolic", q{menu}));
+ $item->show;
+ $author->append($item);
+ }
+
# Favorites of this author
{
my $item = 'Gtk3::ImageMenuItem'->new("Favorites");
@@ -1254,16 +1317,6 @@ sub menu_popup {
#~ }
#>>>
- # Playlists created by this author
- {
- my $item = 'Gtk3::ImageMenuItem'->new("Playlists");
- $item->signal_connect(activate => \&show_playlists_from_selected_author);
- $item->set_property(tooltip_text => "Show playlists created by this author");
- $item->set_image('Gtk3::Image'->new_from_icon_name("emblem-documents-symbolic", q{menu}));
- $item->show;
- $author->append($item);
- }
-
# Liked videos by this author
#<<<
#~ {
@@ -1283,21 +1336,29 @@ sub menu_popup {
$author->append($item);
}
+ my $channel_data = $yv_obj->parse_json_string($liststore->get($iter, 8));
+ my $channel_name = $yv_utils->get_channel_title($channel_data);
+
# Subscribe to channel
{
my $item = 'Gtk3::ImageMenuItem'->new("Subscribe");
- $item->signal_connect(
- activate => sub {
- $yv_obj->subscribe_channel($channel_id)
- or warn "Failed to subscribe to channel <$channel_id>: $!";
- }
- );
+ $item->signal_connect(activate => sub { save_channel_by_id($channel_id, $channel_name, subscribe => 1) });
$item->set_property(tooltip_text => "Subscribe to this channel");
$item->set_image('Gtk3::Image'->new_from_pixbuf($feed_icon_gray_pixbuf));
$item->show;
$author->append($item);
}
+ # Save channel in the user-list
+ {
+ my $item = 'Gtk3::ImageMenuItem'->new("Save channel");
+ $item->set_property(tooltip_text => "Save the channel in the user-list");
+ $item->signal_connect(activate => sub { save_channel_by_id($channel_id, $channel_name) });
+ $item->set_image('Gtk3::Image'->new_from_icon_name("star-new-symbolic", q{menu}));
+ $item->show;
+ $author->append($item);
+ }
+
# Open the YouTube channel page
{
my $item = 'Gtk3::ImageMenuItem'->new("YouTube page");
@@ -1354,15 +1415,15 @@ sub menu_popup {
my ($id, $iter) = get_selected_entry_code();
my $type = $liststore->get($iter, 7);
if (defined($id) and $type eq 'video') {
- execute_cli_fair_viewer("--id=$id --no-video");
+ execute_cli_pipe_viewer("--id=$id --no-video");
}
elsif (defined($id) and $type eq 'playlist') {
- execute_cli_fair_viewer("--pp=$id --no-video");
+ execute_cli_pipe_viewer("--pp=$id --no-video");
}
}
);
$item->set_property(tooltip_text => "Play as audio in a new terminal");
- $item->set_image('Gtk3::Image'->new_from_icon_name("multimedia-audio-player", q{menu}));
+ $item->set_image('Gtk3::Image'->new_from_icon_name("audio-headphones", q{menu}));
$item->show;
$menu->append($item);
}
@@ -1370,13 +1431,12 @@ sub menu_popup {
# Play with CLI fair-viewer
{
my $item = 'Gtk3::ImageMenuItem'->new("Play in terminal");
- $item->signal_connect(activate => \&play_selected_video_with_cli_fair_viewer);
+ $item->signal_connect(activate => \&play_selected_video_with_cli_pipe_viewer);
$item->set_property(tooltip_text => "Play with fair-viewer in a new terminal");
$item->set_image('Gtk3::Image'->new_from_icon_name("computer", q{menu}));
$item->show;
$menu->append($item);
}
-
}
$menu->popup(undef, undef, undef, undef, $event->button, $event->time);
@@ -1385,10 +1445,69 @@ sub menu_popup {
sub users_menu_popup {
my ($treeview, $event) = @_;
+
if ($event->button != 3) {
return 0;
}
- my $menu = $gui->get_object('user_option_menu');
+
+ # Hardcoded menu
+ #my $menu = $gui->get_object('user_option_menu');
+
+ # Dynamic menu
+ my $path = ($treeview->get_path_at_pos($event->x, $event->y))[0] // return 0;
+
+ my $selection = $treeview->get_selection;
+ $selection->select_path($path);
+
+ my $iter = $selection->get_selected() // return 0;
+
+ my $channel_id = $users_liststore->get($iter, 0);
+ my $channel_name = $users_liststore->get($iter, 1);
+
+ # Create the main right-click menu
+ my $menu = 'Gtk3::Menu'->new;
+
+ # Videos from channel
+ {
+ my $item = 'Gtk3::ImageMenuItem'->new("Videos");
+ $item->set_image('Gtk3::Image'->new_from_icon_name("applications-multimedia", q{menu}));
+ $item->set_property(tooltip_text => "List the latest videos from this channel");
+ $item->signal_connect(activate => \&videos_from_selected_username);
+ $item->show;
+ $menu->append($item);
+ }
+
+ # Playlists from channel
+ {
+ my $item = 'Gtk3::ImageMenuItem'->new("Playlists");
+ $item->set_image('Gtk3::Image'->new_from_icon_name("emblem-documents", q{menu}));
+ $item->set_property(tooltip_text => "List the playlists created by this channel");
+ $item->signal_connect(activate => \&playlists_from_selected_username);
+ $item->show;
+ $menu->append($item);
+ }
+
+ # Subscribe / unsubscribe from channel
+ {
+ my $item = 'Gtk3::ImageMenuItem'->new($subscribed_channels{$channel_id} ? "Unsubscribe" : "Subscribe");
+ $subscribed_channels{$channel_id}
+ ? $item->set_image('Gtk3::Image'->new_from_pixbuf($feed_icon_gray_pixbuf))
+ : $item->set_image('Gtk3::Image'->new_from_pixbuf($feed_icon_pixbuf));
+ $item->signal_connect(activate => \&subscribe_toggle_selected_username);
+ $item->show;
+ $menu->append($item);
+ }
+
+ # Remove the channel
+ {
+ my $item = 'Gtk3::ImageMenuItem'->new("Remove");
+ $item->set_image('Gtk3::Image'->new_from_icon_name("gtk-remove", q{menu}));
+ $item->set_property(tooltip_text => "Remove the channel from this list");
+ $item->signal_connect(activate => \&remove_selected_username);
+ $item->show;
+ $menu->append($item);
+ }
+
$menu->popup(undef, undef, undef, undef, $event->button, $event->time);
return 0;
}
@@ -1406,9 +1525,9 @@ set_text(
CTRL+E : enqueue the selected video
CTRL+U : show the saved user-list
- CTRL+D : show more video details for a selected video
+ CTRL+D : show more details for a selected entry
CTRL+W : show the warnings window
- CTRL+G : show videos favorited by the author of a selected video
+ CTRL+G : show favorite videos of the author of a selected video
CTRL+R : show related videos for a selected video
CTRL+M : show videos from the author of a selected video
CTRL+K : show playlists from the author of a selected video
@@ -1419,7 +1538,7 @@ set_text(
F11 : minimize-maximize the main window
HELP_TEXT
- );
+);
{
my $font = Pango::FontDescription::from_string('Monospace 8');
@@ -1436,13 +1555,13 @@ $accel->connect(ord('l'), ['control-mask'], ['visible'], \&show_login_to_youtube
$accel->connect(ord('p'), ['control-mask'], ['visible'], \&show_preferences_window);
$accel->connect(ord('q'), ['control-mask'], ['visible'], \&on_mainw_destroy);
$accel->connect(ord('u'), ['control-mask'], ['visible'], \&show_users_list_window);
-$accel->connect(ord('y'), ['control-mask'], ['visible'], \&run_cli_fair_viewer);
+$accel->connect(ord('y'), ['control-mask'], ['visible'], \&run_cli_pipe_viewer);
$accel->connect(ord('d'), ['control-mask'], ['visible'], \&show_details_window);
#$accel->connect(ord('c'), ['control-mask'], ['visible'], \&show_comments_window);
$accel->connect(ord('s'), ['control-mask'], ['visible'], \&add_user_to_favorites);
$accel->connect(ord('r'), ['control-mask'], ['visible'], \&show_related_videos);
-$accel->connect(ord('g'), ['control-mask'], ['visible'], \&show_user_favorited_videos);
+$accel->connect(ord('g'), ['control-mask'], ['visible'], \&show_user_favorite_videos);
$accel->connect(ord('m'), ['control-mask'], ['visible'], \&show_videos_from_selected_author);
$accel->connect(ord('k'), ['control-mask'], ['visible'], \&show_playlists_from_selected_author);
$accel->connect(ord('w'), ['control-mask'], ['visible'], \&show_warnings_window);
@@ -1575,8 +1694,15 @@ sub hide_login_to_youtube_window {
sub show_details_window {
my ($code, $iter) = get_selected_entry_code();
$code // return;
+
+ my $type = $liststore->get($iter, 7);
+
+ if ($type eq 'next_page') {
+ return 1;
+ }
+
$details_window->show;
- set_entry_details($code, $iter);
+ Glib::Idle->add(sub { set_entry_details($code, $iter); return 0 }, [], Glib::G_PRIORITY_LOW);
return 1;
}
@@ -1611,7 +1737,7 @@ sub show_comments_window {
return 0;
},
[],
- Glib::G_PRIORITY_DEFAULT_IDLE
+ Glib::G_PRIORITY_LOW
);
return 1;
@@ -1755,7 +1881,7 @@ sub toggled_audio_only {
# DASH mode
sub toggled_dash_support {
- $CONFIG{dash_segmented} = $dash_checkbutton->get_active() || 0;
+ $CONFIG{dash} = $dash_checkbutton->get_active() || 0;
}
# Check buttons toggles
@@ -1836,242 +1962,299 @@ sub add_top_row {
);
}
-sub set_youtube_tops {
- my ($top_time, $main_label) = @_;
+my $playlists_liststore = $gui->get_object('liststore6');
+my $playlists_treeview = $gui->get_object('treeview4');
+
+sub add_local_playlist_row {
+ my ($playlist_name, $playlist_file) = @_;
- ...; # Unimplemented!
+ my $iter = $playlists_liststore->append;
- #my $iter = $tops_liststore->append;
- #$tops_liststore->set($iter, 0, "<big><b>\t$main_label</b></big>");
- #add_top_row($name, $type);
+ $playlists_liststore->set(
+ $iter,
+ 0 => encode_entities($playlist_name),
+ 1 => $feed_icon_gray_pixbuf,
+ 2 => $playlist_name,
+ 3 => $playlist_file,
+ );
}
-{
- my %channels;
+sub set_local_playlists {
+ my ($top_time, $main_label) = @_;
- # ------------ Usernames list window ------------ #
- sub set_usernames {
- if (-e $CONFIG{youtube_users_file}) {
- if (open my $fh, '<:utf8', $CONFIG{youtube_users_file}) {
- while (defined(my $entry = <$fh>)) {
+ my @playlist_files = reverse $yv_utils->get_local_playlist_filenames($local_playlists_dir);
- $entry = unpack('A*', $entry);
- my ($channel, $label) = split(' ', $entry, 2);
+ foreach my $file (@playlist_files) {
+ my $snippet = $yv_utils->local_playlist_snippet($file);
+ add_local_playlist_row($yv_utils->get_title($snippet), $file);
+ }
+}
- if (defined($channel) and $channel =~ /$valid_channel_id_re/) {
- $channel = $+{channel_id};
- if (defined($label) and $label =~ /\S/) {
- $channels{$channel} = $label;
- }
- else {
- $channels{$channel} = undef;
- }
- }
- }
- close $fh;
- }
- }
- else {
- # Default channels
- %channels = (
- 'UC1_uAIS3r8Vu6JjXWvastJg' => 'Mathologer',
- 'UCSju5G2aFaWMqn-_0YBtq5A' => 'StandUpMaths',
- 'UCW6TXMZ5Pq6yL6_k5NZ2e0Q' => 'Socratica',
- 'UC-WICcSW1k3HsScuXxDrp0w' => 'Curry On!',
- 'UCShHFwKyhcDo3g7hr4f1R8A' => 'World Science Festival',
- 'UCYO_jab_esuFRV4b17AJtAw' => '3Blue1Brown',
- 'UCWnPjmqvljcafA0z2U1fwKQ' => 'Confreaks',
- 'UC_QIfHvN9auy2CoOdSfMWDw' => 'Strange Loop',
- 'UCH4BNI0-FOK2dMXoFtViWHw' => "It's Okay To Be Smart",
- 'UCHnyfMqiRRG1u-2MsSQLbXA' => 'Veritasium',
- 'UCseUQK4kC3x2x543nHtGpzw' => 'Brian Will',
- 'UC9-y-6csu5WGm29I7JiwpnA' => 'Computerphile',
- 'UCoxcjq-8xIDTYp3uz647V5A' => 'Numberphile',
- 'UC6nSFpj9HTCZ5t-N3Rm3-HA' => 'Vsauce',
- 'UC4a-Gbdw7vOaccHmFo40b9g' => 'Khan Academy',
- 'UCUHW94eEFW7hkUMVaZz4eDg' => 'MinutePhysics',
- 'UCYeF244yNGuFefuFKqxIAXw' => 'The Royal Institution',
- 'UCX6b17PVsYBQ0ip5gyeme-Q' => 'CrashCourse',
- 'UCwbsWIWfcOL2FiUZ2hKNJHQ' => 'UCBerkeley',
- 'UCEBb1b_L6zDS3xTUrIALZOw' => 'MIT OpenCourseWare',
- 'UCAuUUnT6oDeKwE6v1NGQxug' => 'TED',
- 'UCvBqzzvUBLCs8Y7Axb-jZew' => 'Sixty Symbols',
- 'UC6107grRI4m0o2-emgoDnAA' => 'SmarterEveryDay',
- 'UCZYTClx2T1of7BRZ86-8fow' => 'SciShow',
- 'UCF6F8LdCSWlRwQm_hfA2bcQ' => 'Coding Math',
- 'UC1znqKFL3jeR0eoA0pHpzvw' => 'SpaceRip',
- 'UCvjgXvBlbQiydffZU7m1_aw' => 'Daniel Shiffman',
- 'UCC552Sd-3nyi_tk2BudLUzA' => 'AsapSCIENCE',
- 'UC0wbcfzV-bHhABbWGXKHwdg' => 'Utah Open Source',
- 'UCotwjyJnb-4KW7bmsOoLfkg' => 'Art of the Problem',
- 'UC7y4qaRSb5w2O8cCHOsKZDw' => 'YAPC NA',
- );
- }
+set_local_playlists();
- foreach my $channel (sort { ($channels{$a} // lc($a)) cmp($channels{$b} // lc($b)) } keys %channels) {
- my $iter = $users_liststore->append;
+# ------------ Usernames list window ------------ #
+sub set_usernames {
+ if (-e $CONFIG{youtube_users_file}) {
+ %channels = (
+ %channels,
+ (
+ map { @$_ }
+ grep { not exists $removed_channels{$_->[0]} }
+ $yv_utils->read_channels_from_file($CONFIG{youtube_users_file})
+ )
+ );
+ }
+ else {
+ # Default channels
+ %channels = (
+ 'UC1_uAIS3r8Vu6JjXWvastJg' => 'Mathologer',
+ 'UCSju5G2aFaWMqn-_0YBtq5A' => 'Stand-Up Maths',
+ 'UC-WICcSW1k3HsScuXxDrp0w' => 'Curry On!',
+ 'UCShHFwKyhcDo3g7hr4f1R8A' => 'World Science Festival',
+ 'UCYO_jab_esuFRV4b17AJtAw' => '3Blue1Brown',
+ 'UCWnPjmqvljcafA0z2U1fwKQ' => 'Confreaks',
+ 'UC_QIfHvN9auy2CoOdSfMWDw' => 'Strange Loop',
+ 'UCseUQK4kC3x2x543nHtGpzw' => 'Brian Will',
+ 'UC9-y-6csu5WGm29I7JiwpnA' => 'Computerphile',
+ 'UCoxcjq-8xIDTYp3uz647V5A' => 'Numberphile',
+ 'UCvBqzzvUBLCs8Y7Axb-jZew' => 'Sixty Symbols',
+ 'UC6107grRI4m0o2-emgoDnAA' => 'SmarterEveryDay',
+ 'UCF6F8LdCSWlRwQm_hfA2bcQ' => 'Coding Math',
+ 'UC1znqKFL3jeR0eoA0pHpzvw' => 'SpaceRip',
+ 'UCvjgXvBlbQiydffZU7m1_aw' => 'The Coding Train',
+ 'UC0wbcfzV-bHhABbWGXKHwdg' => 'Utah Open Source',
+ 'UCotwjyJnb-4KW7bmsOoLfkg' => 'Art of the Problem',
+ 'UC7y4qaRSb5w2O8cCHOsKZDw' => 'YAPC NA',
+ 'UCGHZpIpAWJQ-Jy_CeCdXhMA' => 'Cool Worlds',
+ 'UCmG6gHgD8JaEZVxuHWJijGQ' => 'UConn Mathematics',
+ 'UC81mayGa63QaJE1SjKIYp0w' => 'metaRising',
+ 'UCSHZKyawb77ixDdsGog4iWA' => 'Lex Fridman',
+ 'UCBa659QWEk1AI4Tg--mrJ2A' => 'Tom Scott',
+ );
+ }
+
+ if (-e $CONFIG{subscribed_channels_file}) {
+ %subscribed_channels = (
+ %subscribed_channels,
+ (
+ map { @$_ }
+ grep { not exists $unsubbed_channels{$_->[0]} }
+ grep { not exists $removed_channels{$_->[0]} }
+ $yv_utils->read_channels_from_file($CONFIG{subscribed_channels_file})
+ )
+ );
+ }
+
+ $users_liststore->clear; # clear the list
+
+ foreach my $channel (sort { CORE::fc($channels{$a} // $a) cmp CORE::fc($channels{$b} // $b) } keys %channels) {
+ my $iter = $users_liststore->append;
- if (defined $channels{$channel}) {
- $users_liststore->set(
- $iter,
- 0 => $channel,
- 1 => $channels{$channel},
- 2 => 'channel',
- );
- }
- else {
- $users_liststore->set(
- $iter,
- 0 => $channel,
- 1 => $channel,
- 2 => 'username',
- );
- }
+ $channels{$channel} // next;
- $users_liststore->set($iter, [3], [$user_icon_pixbuf]);
- }
+ $users_liststore->set(
+ $iter,
+ 0 => $channel,
+ 1 => $channels{$channel},
+ 2 => 'channel',
+ 3 => (
+ exists($subscribed_channels{$channel})
+ ? $feed_icon_pixbuf
+ : $user_icon_pixbuf
+ ),
+ );
}
+}
- sub save_channel {
- my $channel_name = $save_channel_name_entry->get_text;
- my $channel_id = $save_channel_id_entry->get_text;
+sub save_channel {
+ my $channel_name = $save_channel_name_entry->get_text;
+ my $channel_id = $save_channel_id_entry->get_text;
- # Validate the channel id
- if (defined($channel_id) and $channel_id =~ /$valid_channel_id_re/) {
+ # Validate the channel id
+ if (defined($channel_id) and $channel_id =~ /$valid_channel_id_re/) {
- $channel_id = $+{channel_id};
+ $channel_id = $+{channel_id};
- # Get the channel name when empty
- if (not defined($channel_name) or not $channel_name =~ /\S/) {
- $channel_name = $yv_obj->channel_title_from_id($channel_id) // die "Invalid channel ID: <<$channel_id>>";
- }
+ # Get the channel name when empty
+ if (not defined($channel_name) or not $channel_name =~ /\S/) {
+ $channel_name = $yv_obj->channel_title_from_id($channel_id) // die "Invalid channel ID: <<$channel_id>>";
}
- elsif (defined($channel_name) and $channel_name =~ /$valid_channel_id_re/) {
+ }
+ elsif (defined($channel_name) and $channel_name =~ /$valid_channel_id_re/) {
- $channel_name = $+{channel_id};
- $channel_id = $yv_obj->channel_id_from_username($channel_name);
+ $channel_name = $+{channel_id};
+ $channel_id = $yv_obj->channel_id_from_username($channel_name);
- if (not defined $channel_id) {
- die "Can't get channel ID from username: <<$channel_name>>";
- }
- }
- elsif (defined($channel_id) and $channel_id =~ /\S/) {
- die "Invalid channel ID: <<$channel_id>>";
- }
- else {
- return;
+ if (not defined $channel_id) {
+ die "Can't get channel ID from username: <<$channel_name>>";
}
+ }
+ elsif (defined($channel_id) and $channel_id =~ /\S/) {
+ die "Invalid channel ID: <<$channel_id>>";
+ }
+ else {
+ return;
+ }
+
+ save_channel_by_id($channel_id, $channel_name);
+}
+
+sub save_channel_by_id {
+ my ($channel_id, $channel_name, %args) = @_;
- save_channel_by_id($channel_id, $channel_name);
+ # Validate the channel ID
+ if (not defined($channel_id) or not $channel_id =~ /$valid_channel_id_re/) {
+ return;
}
- sub save_channel_by_id {
- my ($channel_id, $channel_name) = @_;
+ if ($channel_id =~ /$valid_channel_id_re/) {
+ $channel_id = $+{channel_id};
+ }
- # Validate the channel ID
- if (not defined($channel_id) or not $channel_id =~ /$valid_channel_id_re/) {
- return;
- }
+ if ($args{subscribe} and not exists($subscribed_channels{$channel_id})) {
+ say ":: Subscribed channel: $channel_name" if $yv_obj->get_debug;
+ $subscribed_channels{$channel_id} = $channel_name;
+ write_channels_to_file(\%subscribed_channels, $CONFIG{subscribed_channels_file});
+ set_usernames();
+ }
- if ($channel_id =~ /$valid_channel_id_re/) {
- $channel_id = $+{channel_id};
- }
+ # Channel ID already exists in the list
+ if (exists($channels{$channel_id})) {
+ return;
+ }
- # Channel ID already exists in the list
- if (exists($channels{$channel_id})) {
- return;
- }
+ # Get the channel name
+ if (not defined($channel_name) or not $channel_name =~ /\S/) {
+ $channel_name = $yv_obj->channel_title_from_id($channel_id) // $channel_id;
+ }
- # Get the channel name
- if (not defined($channel_name) or not $channel_name =~ /\S/) {
- $channel_name = $yv_obj->channel_title_from_id($channel_id) // $channel_id;
- }
+ # Store it internally
+ $channels{$channel_id} = $channel_name;
- # Store it internally
- $channels{$channel_id} = $channel_name;
+ # Append it to the list
+ my $iter = $users_liststore->append;
- # Append it to the list
- my $iter = $users_liststore->append;
+ $users_liststore->set(
+ $iter,
+ 0 => $channel_id,
+ 1 => $channel_name,
+ 2 => 'channel',
+ 3 => $user_icon_pixbuf,
+ );
+}
- $users_liststore->set(
- $iter,
- 0 => $channel_id,
- 1 => $channel_name,
- 2 => 'channel',
- 3 => $user_icon_pixbuf,
- );
- }
+sub add_user_to_favorites {
+ my $selection = $treeview->get_selection() // return;
+ my $iter = $selection->get_selected() // return;
- sub add_user_to_favorites {
- my $channel_id = get_channel_id_for_selected_video() // return;
- save_channel_by_id($channel_id);
- }
+ my $info = $yv_obj->parse_json_string($liststore->get($iter, 8));
- sub remove_selected_user {
- my $selection = $users_treeview->get_selection // return;
- my $iter = $selection->get_selected // return;
- my $channel_id = $users_liststore->get($iter, 0);
- delete $channels{$channel_id};
- $users_liststore->remove($iter);
- }
+ my $channel_id = $liststore->get($iter, 6);
+ my $channel_name = $yv_utils->get_channel_title($info);
- sub save_usernames_to_file {
- open(my $fh, '>:utf8', $CONFIG{youtube_users_file}) or return;
- foreach my $channel (
- sort { ($channels{$a} // $a) cmp($channels{$b} // $b) }
- keys %channels
- ) {
- if (defined($channels{$channel})) {
- say $fh "$channel $channels{$channel}";
- }
- else {
- say $fh $channel;
- }
- }
- close $fh;
+ save_channel_by_id($channel_id, $channel_name);
+}
+
+sub subscribe_toggle_selected_username {
+
+ my $selection = $users_treeview->get_selection // return;
+ my $iter = $selection->get_selected // return;
+
+ my $channel_id = $users_liststore->get($iter, 0);
+ my $channel_name = $users_liststore->get($iter, 1);
+
+ if (exists $subscribed_channels{$channel_id}) {
+ $unsubbed_channels{$channel_id} = 1;
+ delete $subscribed_channels{$channel_id};
+ $users_liststore->set($iter, [3], [$user_icon_pixbuf]);
}
+ else {
+ $subscribed_channels{$channel_id} = $channel_name;
+ delete $unsubbed_channels{$channel_id};
+ $users_liststore->set($iter, [3], [$feed_icon_pixbuf]);
+ }
+
+ write_channels_to_file(\%subscribed_channels, $CONFIG{subscribed_channels_file});
+}
+
+sub remove_selected_username {
+
+ my $selection = $users_treeview->get_selection // return;
+ my $iter = $selection->get_selected // return;
+ my $channel_id = $users_liststore->get($iter, 0);
+
+ delete $channels{$channel_id};
+ delete $subscribed_channels{$channel_id};
+
+ $removed_channels{$channel_id} = 1;
+ $users_liststore->remove($iter);
+}
- # Get playlists from username
- sub playlists_from_selected_username {
- my $selection = $users_treeview->get_selection() // return;
- my $iter = $selection->get_selected() // return;
+sub write_channels_to_file {
+ my ($channels, $file) = @_;
- my $type = $users_liststore->get($iter, 2);
- my $channel = $users_liststore->get($iter, 0);
+ open(my $fh, '>:utf8', $file) or return;
- playlists($type, $channel);
+ foreach my $channel (
+ sort { CORE::fc($channels->{$a} // $a) cmp CORE::fc($channels->{$b} // $b) }
+ keys %$channels
+ ) {
+ if (defined($channels->{$channel})) {
+ say $fh "$channel $channels->{$channel}";
+ }
+ else {
+ say $fh "$channel $channel";
+ }
}
- sub videos_from_selected_username {
- my $selection = $users_treeview->get_selection() // return;
- my $iter = $selection->get_selected() // return;
+ close $fh;
+}
+
+sub save_usernames_to_file {
- my $type = $users_liststore->get($iter, 2);
- my $channel = $users_liststore->get($iter, 0);
+ set_usernames(); # update %channels
- uploads($type, $channel);
+ foreach my $id (keys %removed_channels) {
+ delete $channels{$id};
+ delete $subscribed_channels{$id};
}
- sub videos_from_saved_channel {
- hide_users_list_window();
- videos_from_selected_username();
+ foreach my $id (keys %unsubbed_channels) {
+ delete $subscribed_channels{$id};
}
+
+ write_channels_to_file(\%channels, $CONFIG{youtube_users_file});
+ write_channels_to_file(\%subscribed_channels, $CONFIG{subscribed_channels_file});
}
-# ----- My panel settings ----- #
-sub log_out {
- change_subscription_page(0);
+# Get playlists from username
+sub playlists_from_selected_username {
+ my $selection = $users_treeview->get_selection() // return;
+ my $iter = $selection->get_selected() // return;
+
+ my $type = $users_liststore->get($iter, 2);
+ my $channel = $users_liststore->get($iter, 0);
- unlink $authentication_file
- or warn "Can't unlink: `$authentication_file' -> $!";
+ playlists($type, $channel);
+}
- $yv_obj->set_access_token();
- $yv_obj->set_refresh_token();
+sub videos_from_selected_username {
+ my $selection = $users_treeview->get_selection() // return;
+ my $iter = $selection->get_selected() // return;
- $statusbar->push(1, "Not logged in.");
- return 1;
+ my $type = $users_liststore->get($iter, 2);
+ my $channel = $users_liststore->get($iter, 0);
+
+ uploads($type, $channel);
}
+sub videos_from_saved_channel {
+ hide_users_list_window();
+ videos_from_selected_username();
+}
+
+# ----- My panel settings ----- #
+
sub change_subscription_page {
my ($value) = @_;
foreach my $object (qw(subsc_scrollwindow subsc_label)) {
@@ -2313,12 +2496,24 @@ sub get_code {
my $type = $liststore->get($iter, 7);
- $type eq 'playlist' ? list_playlist($code)
- : ($type eq 'channel' || $type eq 'subscription') ? uploads('channel', $code)
- : $type eq 'next_page' && $code ne '' ? do {
+ if ($type eq 'playlist') {
+ list_playlist($code);
+ }
+ elsif ($type eq 'channel' or $type eq 'subscription') {
+ uploads('channel', $code);
+ }
+ elsif ($type eq 'next_page' and $code ne '') {
+ my $results;
my $next_page_token = $liststore->get($iter, 5);
- my $results = $yv_obj->next_page($code, $next_page_token);
+
+ if ($next_page_token =~ /^json (.*)/s) {
+ my $data = $yv_obj->parse_json_string($1);
+ $results = get_results_from_list(%$data);
+ }
+ else {
+ $results = $yv_obj->next_page($code, $next_page_token);
+ }
if ($yv_utils->has_entries($results)) {
my $label = '<big><b>' . ('=' x 20) . '</b></big>';
@@ -2330,18 +2525,17 @@ sub get_code {
}
display_results($results);
- }
- : $type eq 'video' ? (
- $CONFIG{audio_only}
- ? execute_cli_fair_viewer("--id=$code")
- : play_video($yv_obj->parse_json_string($liststore->get($iter, 8)))
- )
- : ();
+ }
+ elsif ($type eq 'video') {
+ $CONFIG{audio_only}
+ ? execute_cli_pipe_viewer("--id=$code")
+ : play_video($yv_obj->parse_json_string($liststore->get($iter, 8)));
+ }
return 0;
},
[$code, $iter],
- Glib::G_PRIORITY_DEFAULT_IDLE
+ Glib::G_PRIORITY_LOW
);
}
@@ -2350,18 +2544,25 @@ sub make_row_description {
}
sub append_next_page {
- my ($url, $continuation) = @_;
+ my ($url, $token) = @_;
+
+ if (ref($token) ne 'CODE') {
+ $url // return;
+ }
+
+ $token // return; # no next page is available
- $url // return;
my $iter = $liststore->append;
$liststore->set(
$iter,
0 => "<big><b>LOAD MORE</b></big>",
3 => $url,
- 5 => $continuation,
+ 5 => $token,
7 => 'next_page',
);
+
+ return $iter;
}
sub determine_image_format {
@@ -2403,7 +2604,7 @@ sub get_pixbuf_thumbnail_from_content {
require Digest::MD5;
- my $md5 = Digest::MD5::md5_hex($thumbnail);
+ my $md5 = Digest::MD5::md5_hex($thumbnail // return $default_thumb);
my $key = "$md5 $xsize $ysize";
state %cache;
@@ -2487,6 +2688,178 @@ sub get_pixbuf_thumbnail_from_entry {
return $pixbuf;
}
+sub get_results_from_list {
+ my (%args) = @_;
+
+ $args{entries} //= [];
+ $args{page} //= $yv_obj->get_page;
+
+ my @results = @{$args{entries}};
+
+ 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];
+ }
+
+ 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};
+
+ if ($args{page} * $maxResults < $totalResults) {
+ $results{continuation} = 'json '
+ . $yv_obj->make_json_string(
+ {
+ %args, page => $args{page} + 1,
+ }
+ );
+ }
+
+ scalar {results => \%results, url => 'file'};
+}
+
+sub videos_from_data_file {
+ my ($file, %args) = @_;
+
+ my $videos = eval { Storable::retrieve($file) } // [];
+
+ if ($args{reverse}) {
+ $videos = [reverse @$videos];
+ }
+
+ foreach my $entry (@$videos) {
+ if (ref($entry->{timestamp} // '') eq 'Time::Piece') {
+ $entry->{timestamp} = [@{$entry->{timestamp}}];
+ }
+ }
+
+ get_results_from_list(entries => $videos);
+}
+
+sub get_subscription_video_results {
+
+ # 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 $CONFIG{subscribed_channels_file})) {
+ return videos_from_data_file($subscription_videos_data_file);
+ }
+
+ my @channels = $yv_utils->read_channels_from_file($CONFIG{subscribed_channels_file});
+
+ if (not @channels) {
+ warn "\n[!] No subscribed channels...\n";
+ return get_results_from_list(entries => []);
+ }
+
+ my %subscriptions;
+
+ 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]...\n", $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;
+ }
+
+ 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 = $CONFIG{subscriptions_limit} // 1e4;
+ if ($subscriptions_limit > 0 and scalar(@$subscriptions_data) > $subscriptions_limit) {
+ $#$subscriptions_data = $subscriptions_limit;
+ }
+
+ foreach my $entry (@$subscriptions_data) {
+ if (ref($entry->{timestamp} // '') eq 'Time::Piece') {
+ $entry->{timestamp} = [@{$entry->{timestamp}}];
+ }
+ }
+
+ if (@$subscriptions_data) {
+ Storable::store($subscriptions_data, $subscription_videos_data_file);
+ }
+
+ get_results_from_list(entries => $subscriptions_data);
+}
+
+sub get_watched_video_results {
+ videos_from_data_file($watch_history_data_file);
+}
+
+sub display_watched_videos {
+ $liststore->clear if $CONFIG{clear_search_list};
+ display_results(get_watched_video_results());
+}
+
+sub display_subscription_videos {
+ $liststore->clear if $CONFIG{clear_search_list};
+ display_results(get_subscription_video_results());
+}
+
sub display_results {
my ($results, $from_history) = @_;
@@ -2495,9 +2868,6 @@ sub display_results {
#my $info = $results->{results} // {};
my $items = $results->{results} // [];
- #use Data::Dump qw(pp);
- #pp $items;
-
if (ref($items) eq 'HASH') {
if (exists $items->{videos}) {
@@ -2506,22 +2876,22 @@ sub display_results {
elsif (exists $items->{playlists}) {
$items = $items->{playlists};
}
+ elsif (exists $items->{channels}) {
+ $items = $items->{channels};
+ }
+ elsif (exists $items->{entries}) {
+ $items = $items->{entries};
+ }
else {
warn "No results...\n";
}
}
if (ref($items) ne 'ARRAY') {
-
- my $current_instance = $yv_obj->get_api_host();
-
- # Server error. Pick another invidious instance.
- $yv_obj->pick_and_set_random_instance();
-
- die "Probably $current_instance is down.\n"
+ die "Probably the selected invidious instance is down.\n"
. "\nTry changing the `api_host` in configuration file:\n\n"
. qq{\tapi_host => "auto",\n}
- . qq{\nSee also: https://libregit.org/heckyel/fair-viewer#invidious-instances\n};
+ . qq{\nSee also: https://git.sr.ht/~heckyel/fair-viewer#invidious-instances\n};
}
if (not $yv_utils->has_entries($results)) {
@@ -2682,7 +3052,7 @@ sub set_thumbnail {
return 0;
},
[$entry, $liststore, $iter],
- Glib::G_PRIORITY_DEFAULT_IDLE
+ Glib::G_PRIORITY_LOW
);
}
@@ -2701,14 +3071,14 @@ sub add_subscription_entry {
'<big><b>'
. encode_entities($title)
. "</b></big>\n\n"
- . "<b>$symbols{face}\t</b> "
+ . "$symbols{author}\t "
. encode_entities($channel_id) . "\n"
- . "<b>$symbols{crazy_arrow}\t</b> "
- . $yv_utils->get_publication_date($subscription)
+ . "$symbols{published}\t "
+ . ($yv_utils->get_publication_date($subscription) // 'unknown')
. "\n\n<i>"
. encode_entities($row_description) . '</i>';
- my $type_label = "<b>$symbols{diamond}</b> " . 'Subscription' . "\n";
+ my $type_label = "$symbols{type}\t " . 'Subscription' . "\n";
$liststore->set(
$iter,
@@ -2718,6 +3088,7 @@ sub add_subscription_entry {
4 => encode_entities($description),
6 => $channel_id,
7 => 'subscription',
+ 8 => $yv_obj->make_json_string($subscription),
);
if ($CONFIG{show_thumbs}) {
@@ -2742,30 +3113,30 @@ sub add_video_entry {
set_entry_tooltip($iter, $title, $description);
- my $title_label =
- reflow_text( "<big><b>"
- . encode_entities($title)
- . "</b></big>\n"
- . "<b>$symbols{up_arrow}\t</b> "
- . $yv_utils->set_thousands($yv_utils->get_likes($video)) . "\n"
- . "<b>$symbols{down_arrow}\t</b> "
- . $yv_utils->set_thousands($yv_utils->get_dislikes($video)) . "\n"
- . "<b>$symbols{ellipsis}\t</b> "
- . encode_entities($yv_utils->get_category_name($video)) . "\n"
- . "<b>$symbols{face}\t</b> "
- . encode_entities($yv_utils->get_channel_title($video)) . "\n" . "<i>"
- . encode_entities($row_description)
- . "</i>");
-
- my $info_label =
- reflow_text( "<b>$symbols{play}\t</b> "
- . $yv_utils->get_time($video) . "\n"
- . "<b>$symbols{diamond}\t</b> "
- . $yv_utils->get_definition($video) . "\n"
- . "<b>$symbols{views}\t</b> "
- . $yv_utils->set_thousands($yv_utils->get_views($video)) . "\n"
- . "<b>$symbols{right_arrow}\t </b>"
- . $yv_utils->get_publication_date($video));
+ my $title_label = reflow_text(
+ sprintf(
+ "<big><b>%s</b></big>
+
+$symbols{author}\t %s
+$symbols{published}\t %s
+
+<i>%s</i>",
+
+ encode_entities($title),
+ encode_entities($yv_utils->get_channel_title($video)),
+ ($yv_utils->get_publication_date($video) // 'unknown'),
+ encode_entities($row_description),
+ )
+ );
+
+ my $info_label = reflow_text(
+ sprintf(
+ "$symbols{play}\t %s
+$symbols{views}\t %s",
+ $yv_utils->get_time($video),
+ $yv_utils->set_thousands($yv_utils->get_views($video)),
+ )
+ );
$liststore->set(
$iter,
@@ -2794,21 +3165,31 @@ sub add_channel_entry {
set_entry_tooltip($iter, $title, $description);
- my $title_label =
- reflow_text( '<big><b>'
- . encode_entities($title)
- . "</b></big>\n\n"
- . "<b>$symbols{face}\t</b> "
- . encode_entities($yv_utils->get_channel_title($channel)) . "\n"
- . "<b>$symbols{play}\t</b> "
- . encode_entities($channel_id) . "\n"
- . "<b>$symbols{crazy_arrow}\t</b> "
- . $yv_utils->get_publication_date($channel)
- . "\n\n<i>"
- . encode_entities($row_description)
- . '</i>');
-
- my $type_label = reflow_text("<b>$symbols{diamond}</b> " . 'Channel' . "\n");
+ my $title_label = reflow_text(
+ sprintf(
+ "<big><b>%s</b></big>
+
+$symbols{author}\t %s
+$symbols{author_id}\t %s
+
+<i>%s</i>",
+ encode_entities($title),
+ encode_entities($title),
+ encode_entities($channel_id),
+ encode_entities($row_description),
+ )
+ );
+
+ my $type_label = reflow_text(
+ sprintf(
+ "$symbols{type}\t Channel
+$symbols{video}\t %s videos
+$symbols{subs}\t %s subs",
+
+ $yv_utils->set_thousands($yv_utils->get_video_count($channel)),
+ $yv_utils->short_human_number($yv_utils->get_subscriber_count($channel)),
+ )
+ );
$liststore->set(
$iter,
@@ -2818,6 +3199,7 @@ sub add_channel_entry {
4 => encode_entities($description),
6 => $channel_id,
7 => 'channel',
+ 8 => $yv_obj->make_json_string($channel),
);
if ($CONFIG{show_thumbs}) {
@@ -2838,23 +3220,29 @@ sub add_playlist_entry {
set_entry_tooltip($iter, $title, $description);
- my $title_label =
- reflow_text( '<big><b>'
- . encode_entities($title)
- . "</b></big>\n\n"
- . "<b>$symbols{face}\t</b> "
- . encode_entities($channel_title) . "\n"
- . "<b>$symbols{play}\t</b> "
- . encode_entities($playlist_id) . "\n"
- . "<b>$symbols{crazy_arrow}\t</b> "
- . $yv_utils->get_publication_date($playlist) . "\n\n" . '<i>'
- . encode_entities($row_description)
- . '</i>');
-
- my $num_items_template = "<b>$symbols{numero}</b> %d items\n";
- my $num_items_text = sprintf($num_items_template, $yv_utils->get_playlist_video_count($playlist));
-
- my $type_label = reflow_text("<b>$symbols{diamond}</b> " . 'Playlist' . "\n" . $num_items_text);
+ my $title_label = reflow_text(
+ sprintf(
+ "<big><b>%s</b></big>
+
+$symbols{author}\t %s
+$symbols{play}\t %s
+
+<i>%s</i>",
+
+ encode_entities($title),
+ encode_entities($channel_title),
+ encode_entities($playlist_id),
+ encode_entities($row_description),
+ )
+ );
+
+ my $type_label = reflow_text(
+ sprintf(
+ "$symbols{type}\t Playlist
+$symbols{video}\t %s videos",
+ $yv_utils->set_thousands($yv_utils->get_playlist_video_count($playlist) // 0)
+ )
+ );
$liststore->set(
$iter,
@@ -2864,6 +3252,7 @@ sub add_playlist_entry {
4 => encode_entities($description),
6 => $channel_id,
7 => 'playlist',
+ 8 => $yv_obj->make_json_string($playlist),
);
if ($CONFIG{show_thumbs}) {
@@ -2996,12 +3385,12 @@ sub get_streaming_url {
if (ref($captions) eq 'ARRAY' and @$captions and $CONFIG{get_captions}) {
require WWW::FairViewer::GetCaption;
my $yv_cap = WWW::FairViewer::GetCaption->new(
- auto_captions => $CONFIG{auto_captions},
- captions_dir => $CONFIG{cache_dir},
- captions => $captions,
- languages => $CONFIG{srt_languages},
- yv_obj => $yv_obj,
- );
+ auto_captions => $CONFIG{auto_captions},
+ captions_dir => $CONFIG{cache_dir},
+ captions => $captions,
+ languages => $CONFIG{srt_languages},
+ yv_obj => $yv_obj,
+ );
$srt_file = $yv_cap->save_caption($video_id);
}
@@ -3015,9 +3404,11 @@ sub get_streaming_url {
hfr => $CONFIG{hfr},
ignore_av1 => $CONFIG{ignore_av1},
- dash => $CONFIG{dash_support},
- dash_mp4_audio => $CONFIG{dash_mp4_audio},
- dash_segmented => $CONFIG{dash_segmented},
+ split => $CONFIG{split_videos},
+ prefer_m4a => $CONFIG{prefer_m4a},
+ dash => $CONFIG{dash},
+
+ ignored_projections => $CONFIG{ignored_projections},
);
return {
@@ -3083,6 +3474,54 @@ sub get_player_command {
$has_video ? $cmd : join(' ', $cmd, quotemeta($streaming->{streaming}{url}));
}
+sub prepend_video_data_to_file {
+ my ($video_data, $file) = @_;
+
+ my $videos = eval { Storable::retrieve($file) } // [];
+
+ if (ref($video_data) ne 'HASH') {
+ return;
+ }
+
+ $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, $video_data) = @_;
+
+ # Store the video title to history (when `save_watched_to_history` is true)
+ if ($CONFIG{save_watched_to_history}) {
+ append_to_history($yv_utils->get_title($video_data), 0);
+ }
+
+ if ($CONFIG{watch_history}) {
+
+ say ":: Saving video <<$video_id>> to watch history..." if ($yv_obj->get_debug);
+
+ if (not exists($WATCHED_VIDEOS{$video_id})) {
+
+ $WATCHED_VIDEOS{$video_id} = 1;
+
+ open my $fh, '>>', $CONFIG{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;
+ return 1;
+}
+
sub play_video {
my ($video) = @_;
@@ -3111,7 +3550,13 @@ sub play_video {
}
my $code = execute_external_program($command);
- warn "[!] Can't play this video -- player exited with code: $code\n" if $code != 0;
+
+ if ($code == 0) {
+ save_watched_video($video_id, $video);
+ }
+ else {
+ warn "[!] Can't play this video -- player exited with code: $code\n";
+ }
return 1;
}
@@ -3133,32 +3578,16 @@ sub list_category {
}
}
-sub list_tops {
-
- my $iter = $tops_treeview->get_selection->get_selected;
-
- my %top_opts;
- $top_opts{feed_id} = $tops_liststore->get($iter, 2) // return;
- my $top_type = $tops_liststore->get($iter, 3);
+sub list_local_playlist {
+ my $iter = $playlists_treeview->get_selection->get_selected;
- if ($top_type ne q{}) {
- $top_opts{time_id} = $top_type;
- }
-
- if (length(my $region = $gui->get_object('region_entry')->get_text)) {
- $top_opts{region_id} = $region;
- }
-
- if (length(my $category = $gui->get_object('category_entry')->get_text)) {
- $top_opts{cat_id} = $category;
- }
+ my $reverse_playlist = $gui->get_object('reverse_playlist')->get_active;
+ my $playlist_file = $playlists_liststore->get($iter, 3);
$liststore->clear if $CONFIG{clear_search_list};
- display_results(
- $top_type eq 'movies'
- ? $yv_obj->get_movies($top_opts{feed_id})
- : $yv_obj->get_video_tops(%top_opts)
- );
+
+ my $results = videos_from_data_file($playlist_file, reverse => $reverse_playlist);
+ display_results($results);
}
sub clear_text {
@@ -3171,20 +3600,19 @@ sub clear_text {
return 0;
}
-sub run_cli_fair_viewer {
- execute_cli_fair_viewer('--interactive');
+sub run_cli_pipe_viewer {
+ execute_cli_pipe_viewer('--interactive');
}
sub get_options_as_arguments {
my @args;
my %options = (
- 'no-interactive' => q{},
- 'resolution' => $CONFIG{resolution},
- 'download-dir' => quotemeta(rel2abs($CONFIG{downloads_dir})),
- 'fullscreen' => $CONFIG{fullscreen} ? q{} : undef,
- 'no-dash' => $CONFIG{dash_support} ? undef : q{},
- 'no-dash-segmented' => $CONFIG{dash_segmented} ? undef : q{},
- 'no-video' => $CONFIG{audio_only} ? q{} : undef,
+ 'no-interactive' => q{},
+ 'resolution' => $CONFIG{resolution},
+ 'download-dir' => quotemeta(rel2abs($CONFIG{downloads_dir})),
+ 'fullscreen' => $CONFIG{fullscreen} ? q{} : undef,
+ 'no-dash' => $CONFIG{dash} ? undef : q{},
+ 'no-video' => $CONFIG{audio_only} ? q{} : undef,
);
while (my ($argv, $value) = each %options) {
@@ -3254,22 +3682,22 @@ sub enqueue_video {
sub play_enqueued_videos {
if (@VIDEO_QUEUE) {
- execute_cli_fair_viewer('--video-ids=' . join(q{,}, splice @VIDEO_QUEUE));
+ execute_cli_pipe_viewer('--video-ids=' . join(q{,}, splice @VIDEO_QUEUE));
}
return 1;
}
-sub play_selected_video_with_cli_fair_viewer {
+sub play_selected_video_with_cli_pipe_viewer {
my ($code, $iter) = get_selected_entry_code();
$code // return;
my $type = $liststore->get($iter, 7);
if ($type eq 'video') {
- execute_cli_fair_viewer("--video-id=$code");
+ execute_cli_pipe_viewer("--video-id=$code");
}
elsif ($type eq 'playlist') {
- execute_cli_fair_viewer("--pp=$code");
+ execute_cli_pipe_viewer("--pp=$code");
}
else {
warn "Can't play $type: $code\n";
@@ -3278,7 +3706,7 @@ sub play_selected_video_with_cli_fair_viewer {
return 1;
}
-sub execute_cli_fair_viewer {
+sub execute_cli_pipe_viewer {
my @arguments = @_;
my $command = join(
@@ -3287,8 +3715,8 @@ sub execute_cli_fair_viewer {
sprintf(
$CONFIG{terminal_exec},
join(q{ },
- $CONFIG{fair_viewer}, get_options_as_arguments(),
- @arguments, @{$CONFIG{fair_viewer_args}}),
+ $CONFIG{pipe_viewer}, get_options_as_arguments(),
+ @arguments, @{$CONFIG{pipe_viewer_args}}),
)
);
my $code = execute_external_program($command);
@@ -3301,7 +3729,7 @@ sub execute_cli_fair_viewer {
sub download_video {
my $code = get_selected_entry_code(type => 'video') // return;
- execute_cli_fair_viewer("--video-id=$code", '--download');
+ execute_cli_pipe_viewer("--video-id=$code", '--download');
return 1;
}
@@ -3336,7 +3764,7 @@ sub comments_row_activated {
return 1;
}
-sub show_user_favorited_videos {
+sub show_user_favorite_videos {
my $username = get_channel_id_for_selected_video() // return;
favorites('channel', $username);
}
@@ -3394,30 +3822,27 @@ sub display_comments {
foreach my $comment (@{$comments}) {
- #use Data::Dump qw(pp);
- #pp $comment;
-
#my $comment_age = $yv_utils->date_to_age($snippet->{publishedAt});
my $comment_id = $yv_utils->get_comment_id($comment);
my $comment_age = $yv_utils->get_publication_age_approx($comment);
my $comment_text = reflow_text(
- "<big><b>"
- . encode_entities($yv_utils->get_author($comment))
- . "</b> ("
- . (
- $comment_age =~ /sec|min|hour|day/
- ? "$comment_age ago"
- : $yv_utils->get_publication_date($comment)
- )
- . ") commented:</big>\n"
- . encode_entities(
+ sprintf(
+ "<big><b>%s</b> (%s) commented:</big>\n%s",
+ encode_entities($yv_utils->get_author($comment)),
+ (
+ $comment_age =~ /sec|min|hour|day/
+ ? "$comment_age ago"
+ : ($yv_utils->get_publication_date($comment) // 'unknown')
+ ),
+ encode_entities(
wrap_text(
i_tab => "\t",
s_tab => "\t",
text => [$yv_utils->get_comment_content($comment) // 'Empty comment...'],
)
- )
+ ),
+ )
);
my $iter = $feeds_liststore->append;
@@ -3497,7 +3922,6 @@ sub save_session {
$ResultsHistory{current} = $#left + 1;
$ResultsHistory{results} = [@left, $curr_result, @right];
- require Storable;
Storable::store(
{
keyword => $search_entry->get_text,
@@ -3559,30 +3983,89 @@ sub show_playlists_from_selected_author {
}
sub set_entry_details {
- my ($code, $iter) = @_;
+ my ($code, $iter, %opt) = @_;
- my $type = $liststore->get($iter, 7);
- my $main_details = $liststore->get($iter, 0);
- my $channel_id = get_channel_id_for_selected_video();
+ my $type = $liststore->get($iter, 7);
+ my $info = $yv_obj->parse_json_string($liststore->get($iter, 8));
# Setting title
- my $title = substr($main_details, 0, index($main_details, '</big>') + 6, '');
- $gui->get_object('video_title_label')->set_label("<big>$title</big>");
- $gui->get_object('video_title_label')->set_tooltip_markup("$title");
+ my $title = $yv_utils->get_title($info);
+
+ if ($type eq 'channel' or $type eq 'subscription') {
+ $title = $yv_utils->get_channel_title($info);
+ }
+
+ $title = encode_entities($title);
+
+ $gui->get_object('video_title_label')->set_label("<big><big><b>$title</b></big></big>");
+ $gui->get_object('video_title_label')->set_tooltip_markup("<b>$title</b>");
+
+ my $text_info;
- # Setting video details
- $main_details =~ s/^\s+//;
- $main_details =~ s{\s*<i>.+</i>\s*}{\n};
- $main_details =~ s{\h+}{ }g;
- $main_details =~ s{^.*?<b>.*?</b>\K\h*}{\t}gm;
+ if ($type eq 'video') {
- my $secondary_details = $liststore->get($iter, 2);
- $secondary_details =~ s{\h+}{ }g;
- $secondary_details =~ s{^.*?<b>.*?</b>\K\h*}{\t}gm;
- $secondary_details .= "\n$symbols{black_face}\t$channel_id";
+ if ($opt{extra_info}) {
+ my $extra_info = $yv_obj->video_details($yv_utils->get_video_id($info));
- my $text_info = join("\n", grep { !/^&#\w+;$/ } split(/\R/, "$main_details$secondary_details"));
- $gui->get_object('video_details_label')->set_label($text_info);
+ foreach my $key (keys %$extra_info) {
+ $info->{$key} = $extra_info->{$key};
+ }
+ }
+
+ my $details_format = <<"EOT";
+$symbols{author}\t%s
+$symbols{category}\t%s
+$symbols{play}\t%s
+$symbols{average}\t%s
+$symbols{views}\t%s
+$symbols{published}\t%s
+$symbols{author_id}\t%s
+EOT
+
+ $text_info = sprintf($details_format,
+ $yv_utils->get_channel_title($info) // 'unknown',
+ $yv_utils->get_category_name($info) // 'unknown',
+ $yv_utils->get_time($info) // 'unknown',
+ $yv_utils->get_rating($info) // 'unknown',
+ $yv_utils->set_thousands($yv_utils->get_views($info) // 0),
+ $yv_utils->get_publication_date($info) // 'unknown',
+ $yv_utils->get_channel_id($info) // 'unknown',
+ );
+ }
+ elsif ($type eq 'playlist') {
+
+ my $details_format = <<"EOT";
+$symbols{author}\t%s
+$symbols{video}\t%s videos
+$symbols{author_id}\t%s
+$symbols{play}\t%s
+EOT
+
+ $text_info = sprintf($details_format,
+ $yv_utils->get_channel_title($info) // 'unknown',
+ $yv_utils->set_thousands($yv_utils->get_playlist_video_count($info) // 0),
+ $yv_utils->get_channel_id($info) // 'unknown',
+ $yv_utils->get_playlist_id($info) // 'unknown',
+ );
+ }
+ elsif ($type eq 'channel' or $type eq 'subscription') {
+
+ my $details_format = <<"EOT";
+$symbols{author}\t%s
+$symbols{video}\t%s videos
+$symbols{subs}\t%s subscribers
+$symbols{author_id}\t%s
+EOT
+
+ $text_info = sprintf($details_format,
+ $yv_utils->get_channel_title($info) // 'unknown',
+ $yv_utils->short_human_number($yv_utils->get_video_count($info) // 0),
+ $yv_utils->short_human_number($yv_utils->get_subscriber_count($info) // 0),
+ $yv_utils->get_channel_id($info) // 'unknown',
+ );
+ }
+
+ $gui->get_object('video_details_label')->set_label("<big>" . encode_entities("\n" . $text_info) . "</big>");
# Setting the link button
my $url = make_youtube_url($type, $code);
@@ -3591,8 +4074,6 @@ sub set_entry_details {
$linkbutton->set_label($url);
$linkbutton->set_uri($url);
- my $info = $yv_obj->parse_json_string($liststore->get($iter, 8));
-
my %thumbs = (
start => 1,
middle => 2,
@@ -3600,15 +4081,15 @@ sub set_entry_details {
);
# Getting thumbs
- foreach my $type (keys %thumbs) {
+ foreach my $nr (keys %thumbs) {
- $gui->get_object("image$thumbs{$type}")->set_from_pixbuf($default_thumb);
+ $gui->get_object("image$thumbs{$nr}")->set_from_pixbuf($default_thumb);
Glib::Idle->add(
sub {
- my ($type) = @{$_[0]};
+ my ($nr) = @{$_[0]};
- my $url = $yv_utils->get_thumbnail_url($info, $type);
+ my $url = $yv_utils->get_thumbnail_url($info, $nr);
#~ my $thumbnail = $info->{snippet}{thumbnails}{medium};
#~ my $url = $thumbnail->{url};
@@ -3617,21 +4098,32 @@ sub set_entry_details {
## no extra thumbnails available while video is LIVE
}
else {
- $url =~ s{/\w+\.(\w+)\z}{/mq$thumbs{$type}.$1};
+ $url =~ s{/\w+\.(\w+)\z}{/mq$thumbs{$nr}.$1};
+ }
+
+ my ($size_x, $size_y) = (160, 90);
+
+ if ($type eq 'subscription' or $type eq 'channel') {
+ ($size_x, $size_y) = (120, 120);
}
- my $pixbuf = get_pixbuf_thumbnail_from_url($url, 160, 90);
- $gui->get_object("image$thumbs{$type}")->set_from_pixbuf($pixbuf);
+ my $pixbuf = get_pixbuf_thumbnail_from_url($url, $size_x, $size_y);
+ $gui->get_object("image$thumbs{$nr}")->set_from_pixbuf($pixbuf);
return 0;
},
- [$type],
- Glib::G_PRIORITY_DEFAULT_IDLE
+ [$nr],
+ Glib::G_PRIORITY_LOW
);
}
# Setting textview description
- set_text($gui->get_object('description_textview'), decode_entities($liststore->get($iter, 4)));
+ set_text($gui->get_object('description_textview'), $yv_utils->get_description($info));
+
+ if ($type eq 'video' and not $opt{extra_info}) {
+ Glib::Idle->add(sub { set_entry_details($code, $iter, extra_info => 1); return 0 }, [], Glib::G_PRIORITY_LOW);
+ }
+
return 1;
}
@@ -3652,7 +4144,6 @@ $notebook->set_current_page($CONFIG{default_notebook_page});
if ($CONFIG{remember_session} and -f $session_file) {
- require Storable;
my $session = eval { Storable::retrieve($session_file) };
if (ref($session) eq 'HASH') {
@@ -3668,7 +4159,7 @@ if ($CONFIG{remember_session} and -f $session_file) {
return 0;
},
[],
- Glib::G_PRIORITY_DEFAULT_IDLE
+ Glib::G_PRIORITY_LOW
);
}
}
@@ -3682,15 +4173,7 @@ if (@ARGV) {
my $text = join(' ', @ARGV);
$search_entry->set_text($text);
$search_entry->set_position(length($text));
-
- Glib::Idle->add(
- sub {
- search();
- return 0;
- },
- [],
- Glib::G_PRIORITY_DEFAULT_IDLE
- );
+ Glib::Idle->add(sub { search(); return 0 }, [], Glib::G_PRIORITY_LOW);
}
'Gtk3'->main;