aboutsummaryrefslogtreecommitdiffstats
path: root/bin/gtk-fair-viewer
diff options
context:
space:
mode:
authorJesús <heckyel@hyperbola.info>2021-07-09 15:27:16 -0500
committerJesús <heckyel@hyperbola.info>2021-07-09 15:27:16 -0500
commit739c821a54c01816e60eb5f774c8977a1e221ea0 (patch)
treee04a7f5a6fe4d450d43fd45c412f9d415bcb7a7e /bin/gtk-fair-viewer
parentc1322a4e9a1fb0a286dab1277a740072d0ab30f9 (diff)
downloadfair-viewer-739c821a54c01816e60eb5f774c8977a1e221ea0.tar.lz
fair-viewer-739c821a54c01816e60eb5f774c8977a1e221ea0.tar.xz
fair-viewer-739c821a54c01816e60eb5f774c8977a1e221ea0.zip
upstream
Diffstat (limited to 'bin/gtk-fair-viewer')
-rwxr-xr-xbin/gtk-fair-viewer1509
1 files changed, 996 insertions, 513 deletions
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;