diff options
author | Jesús <heckyel@hyperbola.info> | 2021-07-09 15:27:16 -0500 |
---|---|---|
committer | Jesús <heckyel@hyperbola.info> | 2021-07-09 15:27:16 -0500 |
commit | 739c821a54c01816e60eb5f774c8977a1e221ea0 (patch) | |
tree | e04a7f5a6fe4d450d43fd45c412f9d415bcb7a7e /bin/gtk-fair-viewer | |
parent | c1322a4e9a1fb0a286dab1277a740072d0ab30f9 (diff) | |
download | fair-viewer-739c821a54c01816e60eb5f774c8977a1e221ea0.tar.lz fair-viewer-739c821a54c01816e60eb5f774c8977a1e221ea0.tar.xz fair-viewer-739c821a54c01816e60eb5f774c8977a1e221ea0.zip |
upstream
Diffstat (limited to 'bin/gtk-fair-viewer')
-rwxr-xr-x | bin/gtk-fair-viewer | 1509 |
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; |