#!/usr/bin/perl

# Copyright (C) 2010-2016 Trizen <echo dHJpemVueEBnbWFpbC5jb20K | base64 -d>.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
#-------------------------------------------------------
#  GTK Youtube Viewer
#  Created on: 12 September 2010
#  Latest edit on: 27 February 2016
#  Website: http://github.com/trizen/youtube-viewer
#-------------------------------------------------------

use utf8;
use 5.014;

use warnings;
no warnings 'once';

my $DEVEL;    # true in devel mode
use if ($DEVEL = 0), lib => qw(../lib);    # devel only

no if $] >= 5.018, warnings => 'experimental::smartmatch';

use Gtk2 qw(-init);
use File::ShareDir qw(dist_dir);
use File::Spec::Functions qw(
  rel2abs
  catdir
  catfile
  curdir
  updir
  path
  tmpdir
  file_name_is_absolute
  );

binmode(STDOUT, ':utf8');

my $appname  = 'GTK Youtube Viewer';
my $version  = '3.2.1';
my $execname = 'gtk-youtube-viewer';

# Share directory
my $share_dir = $DEVEL ? '../share' : dist_dir('WWW-YoutubeViewer');

sub VIDEO_PART () { 'contentDetails,statistics' }
sub EXTRA_VIDEO_PART () { join(',', 'snippet', VIDEO_PART()) }

# Developer key
my $key = 'aXalQYmzI8gPkMSLyMhpApfMAiU2b23Qz2nE3mq';

# Configuration dir/file
my $home_dir;
my $xdg_config_home = $ENV{XDG_CONFIG_HOME};

if ($xdg_config_home and -d -w $xdg_config_home) {
    require File::Basename;
    $home_dir = File::Basename::dirname($xdg_config_home);

    if (not -d -w $home_dir) {
        $home_dir = curdir();
    }
}
else {
    $home_dir =
         $ENV{HOME}
      || $ENV{LOGDIR}
      || ($^O eq 'MSWin32' ? '\Local Settings\Application Data' : ((getpwuid($<))[7] || `echo -n ~`));

    if (not -d -w $home_dir) {
        $home_dir = curdir();
    }

    $xdg_config_home = catdir($home_dir, '.config');
}

# Configuration dir/file
my $config_dir = catdir($xdg_config_home, 'youtube-viewer');
my $config_file         = catfile($config_dir, "$execname.conf");
my $youtube_users_file  = catfile($config_dir, 'youtube_users.txt');
my $history_file        = catfile($config_dir, 'history.txt');
my $authentication_file = catfile($config_dir, 'reg.dat');

# Create the configuration directory
foreach my $dir ($config_dir) {
    if (not -d $dir) {
        require File::Path;
        File::Path::make_path($dir)
          or warn "[!] Can't create the configuration directory `$dir': $!";
    }
}

## Backwards compatibility for moving the configuration files in the new directory
{
    my $old_config_dir = catdir($xdg_config_home, $execname);
    my $old_config_file        = catfile($old_config_dir, "$execname.conf");
    my $old_youtube_users_file = catfile($old_config_dir, 'youtube_users.txt');

    if (-e $old_config_file and not -e $config_file) {
        require File::Copy;
        File::Copy::move($old_config_file, $config_file)
          or warn "[!] Can't move the configuration file `$old_config_file' to `$config_file': $!";
    }

    if (-e $old_youtube_users_file and not -e $youtube_users_file) {
        require File::Copy;
        File::Copy::move($old_youtube_users_file, $youtube_users_file)
          or warn "[!] Can't move the youtube users file `$old_youtube_users_file' to `$youtube_users_file': $!";
    }
}
## end of compatibility

sub which_command {
    my ($cmd) = @_;

    if (file_name_is_absolute($cmd)) {
        return $cmd;
    }

    state $paths = [path()];
    foreach my $path (@{$paths}) {
        if (-e (my $cmd_path = catfile($path, $cmd))) {
            return $cmd_path;
        }
    }
    return;
}

my %symbols = (
               up_arrow    => '↑',
               down_arrow  => '↓',
               category    => '❖',
               face        => '☺',
               average     => 'x̄',
               ellipsis    => '…',
               play        => '▶',
               views       => '◈',
               heart       => '❤',
               right_arrow => '→',
               crazy_arrow => '↬',
              );

# Main configuration
my %CONFIG = (

    # Combobox values
    active_resolution_combobox          => 0,
    active_safeSearch_combobox          => 1,
    active_more_options_expander        => 0,
    active_panel_account_combobox       => 0,
    active_channel_type_combobox        => 0,
    active_subscriptions_order_combobox => 0,

    video_players => {
                      vlc => {
                              cmd   => q{vlc},
                              srt   => q{--sub-file *SUB*},
                              audio => q{--input-slave *AUDIO*},
                              fs    => q{--fullscreen},
                              arg   => q{--quiet --play-and-exit --no-video-title-show --input-title-format *TITLE*},
                             },
                      mpv => {
                              cmd   => q{mpv},
                              srt   => q{--sub-file *SUB*},
                              audio => q{--audio-file *AUDIO*},
                              fs    => q{--fullscreen},
                              arg   => q{--really-quiet --title *TITLE* --no-ytdl},
                             },
                      mplayer => {
                                  cmd   => q{mplayer},
                                  srt   => q{-sub *SUB*},
                                  audio => q{-audiofile *AUDIO*},
                                  fs    => q{-fs},
                                  arg   => q{-prefer-ipv4 -really-quiet -title *TITLE*},
                                 },
                      smplayer => {
                                   cmd => q{smplayer},
                                   srt => q{-sub *SUB*},
                                   fs  => q{-fullscreen},
                                   arg => q{-close-at-end -media-title *TITLE* *URL*},
                                  },
                     },
    video_player_selected => undef,    # autodetect it later

    # GUI options
    clean_text_entries_on_click => 1,
    show_thumbs                 => 1,
    clear_search_list           => 0,
    default_notebook_page       => 1,
    mainw_size                  => '700x400',
    mainw_maximized             => 0,
    mainw_fullscreen            => 0,

    # Youtube options
    dash_support    => 1,
    dash_mp4_audio  => 0,
    maxResults      => 10,
    resolution      => 'original',
    videoDimension  => undef,
    videoEmbeddable => undef,
    videoLicense    => undef,
    videoSyndicated => undef,
    publishedBefore => undef,
    publishedAfter  => undef,
    hl              => 'en_US',
    cats_region     => 'us',

    # URI options
    thumbnail_type       => 'medium',
    youtube_thumb_url    => 'https://i1.ytimg.com/vi/%s/%s.jpg',
    youtube_video_url    => 'https://www.youtube.com/watch?v=%s',
    youtube_playlist_url => 'https://www.youtube.com/playlist?list=%s',
    youtube_channel_url  => 'https://www.youtube.com/channel/%s',

    # Subtitle options
    srt_languages => ['en', 'es'],
    captions_dir  => tmpdir(),
    get_captions  => 1,
    cache_dir     => undef, # will be defined later

    # Others
    http_proxy => undef,
    debug      => 0,
    fullscreen => 0,

    use_threads            => 0,
    use_threads_for_thumbs => 0,    # this is unstable

    hpaned_position        => 420,
    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'},
    youtube_viewer         => undef,
    youtube_users_file     => $youtube_users_file,
    history                => 1,
    history_limit          => 10_000,
    history_file           => $history_file,
    entry_completion_limit => 10,
);

{
    my $config_documentation = <<"EOD";
#!/usr/bin/perl

# $appname $version - configuration file

EOD

    # Save hash config to file
    sub dump_configuration {
        state $x = require Data::Dump;
        open my $config_fh, '>', $config_file
          or do { warn "[!] Can't open '${config_file}' for write: $!"; return };
        my $dumped_config = q{our $CONFIG = } . Data::Dump::pp(\%CONFIG) . "\n";
        print $config_fh $config_documentation, $dumped_config;
        close $config_fh;
    }
}

# Creating config unless it exists
if (not -e $config_file or -z _) {
    dump_configuration();
}

local $SIG{TERM} = \&on_mainw_destroy;
local $SIG{INT}  = \&on_mainw_destroy;

# Locating the .glade interface file and icons dir
my $glade_file = catfile($share_dir, "$execname.glade");
my $icons_path = catdir($share_dir, 'icons');

# Defining GUI
my $gui = 'Gtk2::Builder'->new;
$gui->add_from_file($glade_file);
$gui->connect_signals(undef);

# -------------  Get GUI objects ------------- #

my %objects = (

    # Windows
    '__MAIN__'          => \my $mainw,
    'users_list_window' => \my $users_list_window,
    'help_window'       => \my $help_window,
    'prefernces_window' => \my $prefernces_window,
    'errors_window'     => \my $errors_window,
    'login_to_youtube'  => \my $login_to_youtube,
    'details_window'    => \my $details_window,
    'aboutdialog1'      => \my $about_window,
    'feeds_window'      => \my $feeds_window,
    'warnings_window'   => \my $warnings_window,

    # Others
    'treeview1'              => \my $users_treeview,
    'feeds_statusbar'        => \my $feeds_statusbar,
    'treeview2'              => \my $treeview,
    'treeview3'              => \my $cat_treeview,
    'feeds_treeview'         => \my $feeds_treeview,
    'liststore1'             => \my $liststore,
    'liststore2'             => \my $users_liststore,
    'liststore4'             => \my $cats_liststore,
    'liststore11'            => \my $feeds_liststore,
    'textview3'              => \my $config_view,
    'warnings_textview'      => \my $warnings_textview,
    'errors_textview'        => \my $errors_textview,
    'search_entry'           => \my $search_entry,
    'statusbar1'             => \my $statusbar,
    'treeviewcolumn2'        => \my $thumbs_column,
    'textview2'              => \my $textview_help,
    'from_author_entry'      => \my $from_author_entry,
    'category_id_entry'      => \my $category_id_entry,
    'more_options_expander'  => \my $more_options_expander,
    'notebook1'              => \my $notebook,
    'comboboxtext9'          => \my $resolution_combobox,
    'comboboxtext8'          => \my $duration_combobox,
    'comboboxtext3'          => \my $caption_combobox,
    'comboboxtext4'          => \my $definition_combobox,
    'comboboxtext5'          => \my $safesearch_combobox,
    'comboboxtext1'          => \my $published_within_combobox,
    'comboboxtext13'         => \my $subscriptions_order_combobox,
    'panel_user_entry'       => \my $panel_user_entry,
    'comboboxtext6'          => \my $panel_account_type_combobox,
    'comboboxtext2'          => \my $order_combobox,
    'comboboxtext7'          => \my $channel_type_combobox,
    'videos_checkbox'        => \my $search_for_videos_checkbox,
    'playlists_checkbox'     => \my $search_for_playlists_checkbox,
    'channels_checkbox'      => \my $search_for_channels_checkbox,
    'spinbutton1'            => \my $spin_results,
    'spinbutton2'            => \my $spin_start_with_page,
    'spinbutton3'            => \my $spin_published_within,
    'thumbs_checkbutton'     => \my $thumbs_checkbutton,
    'fullscreen_checkbutton' => \my $fullscreen_checkbutton,
    'clear_list_checkbutton' => \my $clear_search_list_checkbox,
    'dash_checkbutton'       => \my $dash_checkbutton,
    'gif_spinner'            => \my $gif_spinner,
    'hbox2'                  => \my $hbox2,
);

while (my ($key, $value) = each %objects) {
    my $object = $gui->get_object($key);
    if (defined $object) {
        ${$value} = $object;
    }
    else {
        print STDERR "[WARN] undefined object: $key\n";
    }
}

# __WARN__ handle
local $SIG{__WARN__} = sub {
    my $warning = _strip_edge_spaces(join('', @_));

    return if $warning =~ / at \(eval /;
    return if $warning =~ m'\bunhandled exception in callback:';

    $warning = "[" . localtime(time) . "]: " . $warning . "\n";
    print STDERR $warning;

    set_text($warnings_textview, $warning, append => 1);
};

# __DIE__ handle
local $SIG{__DIE__} = sub {
    my $error = join('', @_);
    my $caller = [caller]->[0];

    # Ignore eval() errors
    return if $error =~ / at \(eval /;

    # Just print the third-party errors,
    # without displaying them to the user.
    if (not $caller =~ /^(?:main\z|WWW::YoutubeViewer\b)/) {
        print STDERR "@_\n";
        return;
    }

    set_text(
        $errors_textview,
        $error . do {
            if ($error =~ m{^Can't locate (.+?)\.pm\b}) {
                my $module = $1;
                $module =~ s{[/\\]+}{::}g;
                return if $module eq 'LWP::UserAgent::Cached';
                "\nThe module $module is required!\n\nTo install it, just type in terminal:\n\tsudo cpan $module\n";
            }
          }
          . "\n=>> Previous warnings:\n" . get_text($warnings_textview)
    );
    warn $error;
    $errors_window->show;
    return 1;
};

#---------------------- LOAD IMAGES ----------------------#
my $app_icon_pixbuf = 'Gtk2::Gdk::Pixbuf'->new_from_file(catfile($icons_path, "$execname.png"));
my $user_icon_pixbuf = 'Gtk2::Gdk::Pixbuf'->new_from_file_at_size(catfile($icons_path, "user.png"),          16,  16);
my $feed_icon_pixbuf = 'Gtk2::Gdk::Pixbuf'->new_from_file_at_size(catfile($icons_path, "feed_icon.png"),     16,  16);
my $default_thumb    = 'Gtk2::Gdk::Pixbuf'->new_from_file_at_size(catfile($icons_path, "default_thumb.jpg"), 120, 90);
my $animation = 'Gtk2::Gdk::PixbufAnimation'->new_from_file(catfile($icons_path, "spinner.gif"));

# Setting application title and icon
$mainw->set_title("$appname $version");
$mainw->set_icon($app_icon_pixbuf);

# Regular expressions
use WWW::YoutubeViewer::RegularExpressions;

our $CONFIG;
require $config_file;    # Load the configuration file

if (ref $CONFIG ne 'HASH') {
    die "ERROR: Invalid configuration file!\n\t\$CONFIG is not an HASH ref!";
}

# Add audio support to players (backwards compatibility)
while (my ($player, $data) = each %{$CONFIG->{video_players}}) {
    if (    exists $CONFIG{video_players}{$player}
        and not exists $data->{audio}
        and exists $CONFIG{video_players}{$player}{audio}) {
        $data->{audio} = $CONFIG{video_players}{$player}{audio};
    }
}

# Get valid config keys
my @valid_keys = grep { exists $CONFIG{$_} } keys %{$CONFIG};
@CONFIG{@valid_keys} = @{$CONFIG}{@valid_keys};

# Define the cache directory
if (not defined $CONFIG{cache_dir}) {

    my $cache_dir =
      ($ENV{XDG_CACHE_HOME} and -d -w $ENV{XDG_CACHE_HOME})
      ? $ENV{XDG_CACHE_HOME}
      : catdir($home_dir, '.cache');

    if (not -d -w $cache_dir) {
        $cache_dir = catdir(curdir(), '.cache');
    }

    $CONFIG{cache_dir} = catdir($cache_dir, 'youtube-viewer');
}

# Create the cache directory (if needed)
if (not -d $CONFIG{cache_dir}) {
    require File::Path;
    File::Path::make_path($CONFIG{cache_dir})
      or warn "[!] Can't create dir `$CONFIG{cache_dir}': $!";
}

{
    my $split_string = sub {
        ((map { s/^[[:punct:]]+//r =~ s/[[:punct:]]+\z//r } split(' ', $_[0])), split(/\W+/, $_[0]));
    };

    my %history_dict;

    sub update_history_dict {
        my (@entries) = @_;

        foreach my $str (@entries) {
            my $str_ref = \$str;

            # Create models from each word of the string
            foreach my $word ($split_string->(lc($str))) {
                my $ref = \%history_dict;
                foreach my $char (split(//, $word)) {
                    $ref = $ref->{$char} //= {};
                    push @{$ref->{values}}, $str_ref;
                }
            }
        }
    }

    my $completion;

    sub analyze_text {
        my ($buffer) = @_;

        $completion // return;
        my $text = lc($buffer->get_text);

        my (@matches, @words);
        foreach my $word ($split_string->($text)) {

            next if $word eq '';

            my $ref = \%history_dict;
            foreach my $char (split(//, $word)) {
                if (exists $ref->{$char}) {
                    $ref = $ref->{$char};
                }
                else {
                    $ref = undef;
                    last;
                }
            }

            if (defined $ref and exists $ref->{values}) {
                push @words,   $word;
                push @matches, @{$ref->{values}};
            }
            else {
                @matches = ();    # don't include partial matches
                last;
            }
        }

        state $x = require List::Util;
        @matches = grep {
            my $lc_str = lc(${$_});
            not defined(List::Util::first(sub { index($lc_str, $_) == -1 }, @words));
        } @matches;

        my %seen;
        my $store = Gtk2::ListStore->new('Glib::String');

        my $i = 0;
        foreach my $str (
            map  { $_->[0] }
            sort { $b->[1] <=> $a->[1] }
            map {
                my $lc_str = lc(${$_});
                [${$_},

                 (    # Calculate a score for each match
                    ((($lc_str =~ s/\W+//gr) ^ ($text =~ s/\W+//gr)) =~ /^[\0]+/ ? $+[0]**2 : 0) +
                      scalar(grep { $lc_str =~ /\b\Q$_\E\b/ } @words)**2 +
                      scalar(grep { $lc_str =~ /\b\Q$_\E/ } @words)
                 )
                ]
            } grep { !$seen{$_}++ } @matches
          ) {
            $store->set($store->append, 0, $str);
            last if ++$i == $CONFIG{entry_completion_limit};
        }

        $completion->set_model($store);
    }

    my %history;
    my $history_fh;

    sub set_history {
        defined($history_fh) && return 1;

        # Open the history file for appending
        if (open($history_fh, '>>:utf8', $CONFIG{history_file})) {
            select((select($history_fh), $| = 1)[0]);    # autoflush
        }
        else {
            warn "[!] Can't open history file `$CONFIG{history_file}' for appending: $!";
            return;
        }

        # Slurp the history file into memory
        my @history;
        if (open(my $fh, '<:utf8', $CONFIG{history_file})) {
            while (defined(my $line = <$fh>)) {

                chomp $line;

                if (not exists $history{lc($line)}) {
                    undef $history{lc($line)};
                    push @history, $line;
                }
            }
        }

        # Set entry completion
        $completion = Gtk2::EntryCompletion->new;
        $completion->set_match_func(sub { 1 });
        $completion->set_text_column(0);
        $search_entry->set_completion($completion);

        # Create the completion dictionary
        update_history_dict(@history);

        # Trim the history file to a random number of entries when the limit has been reached
        if ($CONFIG{history_limit} > 0 and $#history >= $CONFIG{history_limit}) {

            # Try to create a backup, first
            require File::Copy;
            File::Copy::cp($CONFIG{history_file}, "$CONFIG{history_file}.bak");

            # Now, try to rewrite the history file
            if (open(my $fh, '>:utf8', $CONFIG{history_file})) {
                say {$fh}
                  join("\n", @history[(@history - $CONFIG{history_limit}) + int(rand($CONFIG{history_limit})) .. $#history]);
                close $fh;
            }
        }

        return 1;
    }

    sub append_to_history {
        my ($text) = @_;

        my $str = join(' ', split(' ', $text));
        if (not exists $history{lc($str)}) {
            if (set_history()) {
                say {$history_fh} $str;
            }
            undef $history{$str};
            update_history_dict($str);
        }
    }
}

# Locate video player
if (not defined $CONFIG{video_player_selected}) {
    foreach my $key (sort keys %{$CONFIG{video_players}}) {
        if (defined(my $abs_player_path = which_command($CONFIG{video_players}{$key}{cmd}))) {
            $CONFIG{video_players}{$key}{cmd} = $abs_player_path;
            $CONFIG{video_player_selected} = $key;
            last;
        }
    }
}

if (not \%CONFIG ~~ $CONFIG) {
    dump_configuration();
}

# Locate a terminal
if (not defined $CONFIG{terminal}) {
    foreach my $term (
                      'gnome-terminal', 'lxterminal', 'terminal', 'xfce4-terminal',
                      'sakura',         'lilyterm',   'evilvte',  'superterm',
                      'terminator',     'kterm',      'mlterm',   'mrxvt',
                      'rxvt',           'urxvt',      'termit',   'fbterm',
                      'stjerm',         'yakuake',    'roxterm',  'xterm'
      ) {
        if (defined(my $abs_path = which_command($term))) {
            $CONFIG{terminal} = $abs_path;
            last;
        }
    }

    $CONFIG{terminal} //= $ENV{TERM} || 'xterm';
}

{
    my $i = length $key;
    $key =~ s/(.{$i})(.)/$2$1/g while --$i;
}

# Locate youtube-viewer
$CONFIG{youtube_viewer} //= which_command('youtube-viewer') // 'youtube-viewer';

use WWW::YoutubeViewer v3.2.1;
my $yv_obj = WWW::YoutubeViewer->new(
                                     escape_utf8         => 1,
                                     key                 => $key,
                                     config_dir          => $config_dir,
                                     hl                  => $CONFIG{hl},
                                     cache_dir           => $CONFIG{cache_dir},
                                     authentication_file => $authentication_file,
                                    );

if (defined $yv_obj->get_access_token()) {
    show_user_panel();
}
else {
    $statusbar->push(1, 'Not logged');
}

{
    $yv_obj->set_client_id('923751928481.apps.googleusercontent.com');
    $yv_obj->set_client_secret("\26/Ae]3\b\6\x186a:*#0\32\t\f\n\27\17GC`" ^ substr($key, -24));
    $yv_obj->set_redirect_uri('urn:ietf:wg:oauth:2.0:oob');
}

require WWW::YoutubeViewer::Utils;
my $yv_utils = WWW::YoutubeViewer::Utils->new(thousand_separator => $CONFIG{thousand_separator},
                                              youtube_url_format => $CONFIG{youtube_video_url},);

# Set default combobox values
$definition_combobox->set_active(0);
$duration_combobox->set_active(0);
$caption_combobox->set_active(0);
$order_combobox->set_active(0);

# Spin button start with page
$spin_start_with_page->set_value(1);

# Set search for videos
$search_for_videos_checkbox->set_active(1);

# Set config file to $CONFIG hash ref
sub apply_configuration {

    # Fullscreen mode
    $fullscreen_checkbutton->set_active($CONFIG{fullscreen});

    # DASH mode
    $dash_checkbutton->set_active($CONFIG{dash_support});

    $clear_search_list_checkbox->set_active($CONFIG{clear_search_list});
    $panel_account_type_combobox->set_active($CONFIG{active_panel_account_combobox});
    $channel_type_combobox->set_active($CONFIG{active_channel_type_combobox});
    $subscriptions_order_combobox->set_active($CONFIG{active_subscriptions_order_combobox});

    $published_within_combobox->set_active(0);

    # Others
    foreach my $option_name (
                             qw(
                             videoSyndicated
                             maxResults videoDimension
                             videoEmbeddable videoLicense
                             publishedAfter publishedBefore
                             regionCode videoCategoryId
                             debug http_proxy
                             )
      ) {

        if (defined $CONFIG{$option_name}) {
            my $code      = \&{"WWW::YoutubeViewer::set_$option_name"};
            my $value     = $CONFIG{$option_name};
            my $set_value = $yv_obj->$code($value);

            if (not defined($set_value) or $set_value ne $value) {
                warn "[!] Invalid value <$value> for option <$option_name>.\n";
            }
        }
    }

    # Spin button results setting config value
    $spin_results->set_value($CONFIG{maxResults});

    # Checking thumbs button
    $thumbs_checkbutton->set_active($CONFIG{show_thumbs});

    # Setup threads
    if ($CONFIG{use_threads}) {
        set_threads();
    }

    # Set the "More options" expander
    $more_options_expander->set_expanded($CONFIG{active_more_options_expander});

    # Combo boxes setting config value
    $resolution_combobox->set_active($CONFIG{active_resolution_combobox});
    $safesearch_combobox->set_active($CONFIG{active_safeSearch_combobox});

    # Resize the main window
    $mainw->set_default_size(split(/x/i, $CONFIG{mainw_size}, 2));
    $mainw->reshow_with_initial_size;

    if ($CONFIG{mainw_maximized}) {
        $mainw->maximize();
    }

    if ($CONFIG{mainw_fullscreen}) {
        maximize_unmaximize_mainw();
    }

    # Support for history input
    if ($CONFIG{history}) {
        set_history();
    }

    # Set HPaned position
    $hbox2->set_position($CONFIG{hpaned_position});

    # Select text from text entry
    $search_entry->select_region(0, -1);
}

# Apply the configuration file
apply_configuration();

# YouTube usernames
my %users_table = map { lc($_) => $_ } (
                                        'KhanAcademy',         'VSauce',           'Gotbletu',      'ScienceChannel',
                                        'SpaceRip',            'TEDtalksDirector', 'MIT',           'SixtySymbols',
                                        'SciShow',             'NumberPhile',      'ComputerPhile', 'UCBerkeley',
                                        'UCTelevision',        'BigThink',         'ProfessorFink', 'UCTVSeminars',
                                        '1Veritasium',         'MinutePhysics',    'BrianTWill',    'SingingBanana',
                                        'TheRoyalInstitution', 'ItsOkayToBeSmart', 'CrashCourse',   'MyCodeSchool'
                                       );
set_usernames();

sub donate {
    my $url = 'https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=75FUVBE6Q73T8';
    system "xdg-open \Q$url\E &";
}

# ---------------- Threads ---------------- #
my ($queue, $jobs);

sub set_threads {
    return 1 if defined $queue;

    $gif_spinner->set_from_animation($animation);
    warn "* Initializing threads...\n";

    require threads;
    require Thread::Queue;

    no warnings 'redefine';
    state $lwp_get = \&WWW::YoutubeViewer::lwp_get;
    *WWW::YoutubeViewer::lwp_get = \&threads_lwp_get;

    $queue = 'Thread::Queue'->new;
    $jobs  = 'Thread::Queue'->new;
    threads->create(
        sub {
            while (defined(my $url = $jobs->dequeue)) {
                $queue->enqueue($yv_obj->$lwp_get($url) || q{});
            }
        }
    )->detach();
}

sub threads_lwp_get {
    my ($self, $url) = @_;

    set_threads() unless defined $queue;
    $gif_spinner->show;

    if (not defined $url) {
        $url = $self;
    }

    $jobs->enqueue($url);
    while ($queue->pending == 0) {
        'Gtk2'->main_iteration;
        if (defined(my $lwp_result = $queue->dequeue_nb)) {
            $gif_spinner->hide;
            return $lwp_result;
        }
    }

    undef $queue;
    undef $jobs;
}

# Set text to a 'textview' object
sub set_text {
    my ($object, $text, %args) = @_;
    my $object_buffer = $object->get_buffer;

    if ($args{append}) {
        my $iter = $object_buffer->get_end_iter;
        $object_buffer->insert($iter, $text);
    }
    else {
        $object_buffer->set_text($text);
    }
    $object->set_buffer($object_buffer);
    return 1;
}

# Get text from a 'textview' object
sub get_text {
    my ($object)      = @_;
    my $object_buffer = $object->get_buffer;
    my $start_iter    = $object_buffer->get_start_iter;
    my $end_iter      = $object_buffer->get_end_iter;
    return $object_buffer->get_text($start_iter, $end_iter, undef);
}

sub new_image_from_pixbuf {
    my ($object_name, $pixbuf) = @_;
    my $object = $gui->get_object($object_name) // return;
    return scalar($object->new_from_pixbuf($pixbuf));
}

# Setting application icons
{
    $gui->get_object('username_list')->set_image(new_image_from_pixbuf('icon_from_pixbuf', $user_icon_pixbuf));
    $gui->get_object('uploads_button')->set_image(new_image_from_pixbuf('icon_from_pixbuf', $user_icon_pixbuf));
    $gui->get_object('button6')->set_image(new_image_from_pixbuf('icon_from_pixbuf', $feed_icon_pixbuf));
    $gui->get_object('button23')->set_image(new_image_from_pixbuf('icon_from_pixbuf', $feed_icon_pixbuf));
}

# Treeview signals
{
    $treeview->signal_connect('button_press_event', \&menu_popup);
    $users_treeview->signal_connect('button_press_event', \&users_menu_popup);
}

# Menu popup
sub menu_popup {
    my ($treeview, $event) = @_;

    #return 0 unless $treeview->get_selection->get_selected();

    if ($event->button != 3) {
        return 0;
    }
    my $menu = $gui->get_object('detailsmenu');
    $menu->popup(undef, undef, undef, undef, $event->button, $event->time);
    return 0;
}

sub users_menu_popup {
    my ($treeview, $event) = @_;
    if ($event->button != 3) {
        return 0;
    }
    my $menu = $gui->get_object('user_option_menu');
    $menu->popup(undef, undef, undef, undef, $event->button, $event->time);
    return 0;
}

# Setting help text
set_text(
    $textview_help, <<"HELP_TEXT"
* Links
    main website: https://code.google.com/p/trizen/
    development website: https://github.com/trizen/youtube-viewer
    developer's website: http://trizen.go.ro

* Developer
    Trizen <trizenx\@gmail.com>

* Contributor
    Ovidiu D. Ni\x{21b}an <nitanovidiu\@gmail.com>

* Config file
    $config_file

* Users list
    $CONFIG{youtube_users_file}

* Key binds

-Main window
CTRL+H : help window
CTRL+L : login window
CTRL+P : preferences window
CTRL+U : username list window
CTRL+Y : CLI youtube viewer
CTRL+D : video details window
CTRL+F : show feeds window
CTRL+W : show the warnings window
CTRL+G : show videos favorited by 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
CTRL+S : add the author name of a selected video into the users list
CTRL+Q : close the application
DEL : remove the selected video from the list
F11 : minimize-maximize the main window

-Preferences window
CTRL+S : save the configuration

-Other windows
ESC : close the focused window

* Configuration

use_threads
    1 to use threads when getting the XML content

use_threads_for_thumbs
    1 to use threads when getting the thumbnails (not recommended!)

srt_languages
    a list with the preferred subtitle languages

captions_dir
    the directory where to store the closed captions from YouTube (.srt files)

* Knowledge:
    http://code.google.com/intl/ro/apis/youtube/2.0/developers_guide_protocol_api_query_parameters.html
HELP_TEXT
        );

# ------------------- Accels ------------------- #

# Main window
my $accel = Gtk2::AccelGroup->new;
$accel->connect(ord('h'), ['control-mask'], ['visible'], \&show_help_window);
$accel->connect(ord('l'), ['control-mask'], ['visible'], \&show_login_to_youtube_window);
$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_youtube_viewer);
$accel->connect(ord('d'), ['control-mask'], ['visible'], \&show_details_window);
$accel->connect(ord('f'), ['control-mask'], ['visible'], \&show_feeds_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'], \&get_user_favorited_videos);
$accel->connect(ord('m'), ['control-mask'], ['visible'], \&show_more_videos_from_username);
$accel->connect(ord('k'), ['control-mask'], ['visible'], \&show_playlists_from_username);
$accel->connect(ord('w'), ['control-mask'], ['visible'], \&show_warnings_window);
$accel->connect(0xffff,   ['lock-mask'],    ['visible'], \&delete_selected_row);
$accel->connect(0xffc8,   ['lock-mask'],    ['visible'], \&maximize_unmaximize_mainw);
$mainw->add_accel_group($accel);

# Other windows (ESC key to close them)
$accel = Gtk2::AccelGroup->new;
$accel->connect(0xff1b, ['lock-mask'], ['visible'], \&hide_users_list_window);
$users_list_window->add_accel_group($accel);

$accel = Gtk2::AccelGroup->new;
$accel->connect(0xff1b, ['lock-mask'], ['visible'], \&hide_feeds_window);
$feeds_window->add_accel_group($accel);

$accel = Gtk2::AccelGroup->new;
$accel->connect(0xff1b,   ['lock-mask'],    ['visible'], \&hide_preferences_window);
$accel->connect(ord('s'), ['control-mask'], ['visible'], \&save_configuration);
$prefernces_window->add_accel_group($accel);

$accel = Gtk2::AccelGroup->new;
$accel->connect(0xff1b, ['lock-mask'], ['visible'], \&hide_help_window);
$help_window->add_accel_group($accel);

$accel = Gtk2::AccelGroup->new;
$accel->connect(0xff1b, ['lock-mask'], ['visible'], \&hide_details_window);
$details_window->add_accel_group($accel);

# ------------------ Authentication ------------------ #

sub show_user_panel {
    change_subscription_page(1);
    $statusbar->push(1, "Logged.");
    return 1;
}

{
    my $get_code_url = $yv_obj->get_accounts_oauth_url();

    # Setting the URL to get the authentication key
    $gui->get_object('get_auth_link_button')->set_uri($get_code_url);

    sub authenticate {
        my $code = $gui->get_object('auth_token_entry')->get_text;

        hide_login_to_youtube_window();

        if ($code ne q{}) {
            my $info = $yv_obj->oauth_login($code) // do {
                warn "Can't login... That's all I know...\n";
                return;
            };

            if (defined $info->{access_token}) {

                $yv_obj->set_access_token($info->{access_token})   // return;
                $yv_obj->set_refresh_token($info->{refresh_token}) // return;

                if ($gui->get_object('login_check_button')->get_active) {
                    $yv_obj->set_authentication_file($authentication_file);
                    $yv_obj->save_authentication_tokens()
                      or warn "Can't store the authentication tokens: $!";
                }
                else {
                    $yv_obj->set_authentication_file();
                }

                show_user_panel();
                return 1;
            }
        }
        return;
    }
}

# ------------------ Showing/Hidding windows ------------------ #

# Main window
sub maximize_unmaximize_mainw {
    state $maximized = 0;
    $maximized++ % 2
      ? $mainw->unfullscreen
      : $mainw->fullscreen;
}

# Users list window
sub show_users_list_window {
    $users_list_window->show;
    return 1;
}

sub hide_users_list_window {
    $users_list_window->hide;
    return 1;
}

# Help window
sub show_help_window {
    $help_window->show;
    return 1;
}

sub hide_help_window {
    $help_window->hide;
    return 1;
}

# Warnings window

sub show_warnings_window {
    $warnings_window->show;
    return 1;
}

sub hide_warnings_window {
    $warnings_window->hide;
    return 1;
}

# About Window
sub show_about_window {
    $about_window->set_program_name("$appname $version");
    $about_window->set_logo($app_icon_pixbuf);
    $about_window->set_resizable(1);
    $about_window->show;
    return 1;
}

sub hide_about_window {
    $about_window->hide;
    return 1;
}

# Error window
sub hide_errors_window {
    $errors_window->hide;
    return 1;
}

# Login window
sub show_login_to_youtube_window {
    $login_to_youtube->show;
    return 1;
}

sub hide_login_to_youtube_window {
    $login_to_youtube->hide;
    return 1;
}

# Details window
sub show_details_window {
    my ($code, $iter) = get_selected_entry_code();
    $code // return;

    #return unless $code =~ /$valid_video_id_re/;
    $details_window->show;
    set_entry_details($code, $iter);
    return 1;
}

sub hide_details_window {
    $details_window->hide;
    return 1;
}

sub set_comments {
    my $videoID = get_selected_entry_code() // return;

    return unless $videoID =~ /$valid_video_id_re/;

    $feeds_liststore->clear;
    display_comments($yv_obj->comments_from_video_id($videoID));
}

# Feeds window
sub show_feeds_window {
    my $videoID = get_selected_entry_code() // return;

    return unless $videoID =~ /$valid_video_id_re/;

    $feeds_window->show;
    $feeds_statusbar->pop(0);

    display_comments($yv_obj->comments_from_video_id($videoID));

    return 1;
}

sub hide_feeds_window {
    $feeds_liststore->clear;
    $feeds_window->hide;
    return 1;
}

# Preferences window
sub show_preferences_window {
    state $x = require Data::Dump;
    get_main_window_size();
    my $config_view_buffer = $config_view->get_buffer;
    $config_view_buffer->set_text(Data::Dump::dump(\%CONFIG));
    $config_view->set_buffer($config_view_buffer);
    $prefernces_window->show;
    return 1;
}

sub hide_preferences_window {
    $prefernces_window->hide;
    return 1;
}

# Save plaintext config to file
sub save_configuration {
    my $config = get_text($config_view);

    my $hash_ref = eval $config;

    print STDERR $@ if $@;
    die $@ if $@;

    %CONFIG = %{$hash_ref};
    dump_configuration();

    apply_configuration();
    hide_preferences_window();
    return 1;
}

sub delete_selected_row {
    my (undef, $iter) = get_selected_entry_code();
    $iter // return;
    $liststore->remove($iter);
    return 1;
}

# Combo boxes changes
sub combobox_order_changed {
    $yv_obj->set_order($order_combobox->get_active_text);
}

sub combobox_resolution_changed {
    $CONFIG{active_resolution_combobox} = $resolution_combobox->get_active;
    my $res = $resolution_combobox->get_active_text;
    $CONFIG{resolution} = $res =~ /^(\d+)p\z/ ? $1 : $res;
}

sub combobox_safesearch_changed {
    $CONFIG{active_safeSearch_combobox} = $safesearch_combobox->get_active;
    $yv_obj->set_safeSearch($safesearch_combobox->get_active_text);
}

sub combobox_duration_changed {
    my $text = $duration_combobox->get_active_text;
    $yv_obj->set_videoDuration($text);
}

sub combobox_caption_changed {
    my $text = $caption_combobox->get_active_text;
    $yv_obj->set_videoCaption($text);
}

sub combobox_subscriptions_order_changed {
    $CONFIG{active_subscriptions_order_combobox} = $subscriptions_order_combobox->get_active;
    $yv_obj->set_subscriptions_order($subscriptions_order_combobox->get_active_text);
}

sub combobox_panel_account_changed {
    my $text = $panel_account_type_combobox->get_active_text;
    $CONFIG{active_panel_account_combobox} = $panel_account_type_combobox->get_active;
    if (not $text =~ /^(?:user|channel)/i) {
        $panel_user_entry->hide;
    }
    else {
        $panel_user_entry->show;
    }
}

sub combobox_channel_type_changed {
    $CONFIG{active_channel_type_combobox} = $channel_type_combobox->get_active;
}

sub combobox_definition_changed {
    my $text = $definition_combobox->get_active_text;
    $yv_obj->set_videoDefinition($text);
}

sub combobox_published_within_changed {
    my $text = $published_within_combobox->get_active_text;

    if ($text =~ /^any/) {
        $spin_published_within->hide;
        $yv_obj->set_publishedAfter(undef);
    }
    else {
        $spin_published_within->show;
    }
}

# Spin buttons changes
sub spin_results_per_page_changed {
    $yv_obj->set_maxResults($CONFIG{maxResults} = $spin_results->get_value);
}

sub spin_start_with_page_changed {
    $yv_obj->set_page($spin_start_with_page->get_value);
}

sub toggled_clear_search_list {
    $CONFIG{clear_search_list} = $clear_search_list_checkbox->get_active() || 0;
}

# Fullscreen mode
sub toggled_mplayer_fullscreen {
    $CONFIG{fullscreen} = $fullscreen_checkbutton->get_active() || 0;
}

# DASH mode
sub toggled_dash_support {
    $CONFIG{dash_support} = $dash_checkbutton->get_active() || 0;
}

# Check buttons toggles
sub thumbs_checkbutton_toggled {
    $CONFIG{show_thumbs} = ($_[0]->get_active() || 0);
    $thumbs_column->set_visible($CONFIG{show_thumbs});
}

# "More options" expander
sub activate_more_options_expander {
    $CONFIG{active_more_options_expander} = $_[0]->get_expanded() ? 0 : 1;
}

# Get main window size
sub get_main_window_size {
    $CONFIG{mainw_size} = join('x', $mainw->get_size);
}

sub main_window_state_events {
    my (undef, $state) = @_;

    my $windowstate = $state->new_window_state();
    my @states = split(' ', $windowstate);

    $CONFIG{mainw_maximized}  = 'maximized'  ~~ \@states ? 1 : 0;
    $CONFIG{mainw_fullscreen} = 'fullscreen' ~~ \@states ? 1 : 0;

    return 1;
}

sub add_category_header {
    my ($text) = @_;
    my $iter = $cats_liststore->append;
    $cats_liststore->set($iter, 0, "<big><b>\t$text</b></big>");
    return 1;
}

sub append_categories {
    my ($categories, $type) = @_;

    foreach my $category (@{$categories->{items}}) {

        # Ignore unassignable categories
        $category->{snippet}{assignable} || next;

        my $label = $yv_utils->get_title($category);
        my $id    = $category->{id};

        $label =~ s{&}{&amp;}g;

        my $iter = $cats_liststore->append;
        $cats_liststore->set($iter, 0, $label);
        $cats_liststore->set($iter, 1, $id);
        $cats_liststore->set($iter, 2, $feed_icon_pixbuf);
        $cats_liststore->set($iter, 3, $type);
    }
    return 1;
}

{
    # Standard categories:
    add_category_header("Categories");

    my $cats = $yv_obj->video_categories($CONFIG{cats_region});
    if (ref($cats) eq 'HASH' and ref($cats->{items}) eq 'ARRAY') {

        my $help_text = '';
        foreach my $cat (sort { $a->{id} <=> $b->{id} } @{$cats->{items}}) {
            $cat->{snippet}{assignable} || next;
            $help_text .= sprintf("%2d - %s\n", $cat->{id}, $yv_utils->get_title($cat));
        }

        # Set tooltip text for "CategoryID" entry
        chomp($help_text);
        $category_id_entry->set_tooltip_text($help_text);

        # Append the categories to the "Categories" tab
        append_categories($cats, 'cat');
    }

    # EDU categories:
    #add_category_header("EDU Categories");
    #append_categories($yv_obj->get_educategories(), 'edu-cat');
}

my $tops_liststore = $gui->get_object('liststore6');
my $tops_treeview  = $gui->get_object('treeview4');

sub add_top_row {
    my ($top_name, $top_type) = @_;
    (my $top_label = ucfirst $top_name) =~ tr/_/ /;
    my $iter = $tops_liststore->append;
    $tops_liststore->set($iter, 0, $top_label);
    $tops_liststore->set($iter, 1, $feed_icon_pixbuf);
    $tops_liststore->set($iter, 2, $top_name);
    $tops_liststore->set($iter, 3, $top_type);
}

sub set_youtube_tops {
    my ($top_time, $main_label) = @_;

    ...;    # Unimplemented!

    #my $iter = $tops_liststore->append;
    #$tops_liststore->set($iter, 0, "<big><b>\t$main_label</b></big>");
    #add_top_row($name, $type);
}

# ------------ Usernames list window ------------ #
sub set_usernames {
    if (-e $CONFIG{youtube_users_file}) {
        if (open my $fh, '<', $CONFIG{youtube_users_file}) {
            while (defined(my $user = <$fh>)) {
                chomp $user;
                $users_table{lc $user} = $user;
            }
            close $fh;
        }
    }
    foreach my $user (sort { lc $a cmp lc $b } values %users_table) {
        my $iter = $users_liststore->append;
        $users_liststore->set($iter, 0, $user);
        $users_liststore->set($iter, 1, $user_icon_pixbuf);
    }
}

sub add_username {
    my $user = $gui->get_object('username_entry')->get_text;
    $users_table{lc $user} = $user;
    my $iter = $users_liststore->append;
    $users_liststore->set($iter, 0, $user);
    $users_liststore->set($iter, 1, $user_icon_pixbuf);
}

sub add_user_to_favorites {
    my $user = get_channel_id_for_selected_video() or return;
    $gui->get_object('username_entry')->set_text($user);
    add_username();
    $feeds_statusbar->push(0, "Successfully added '${user}' into the username list (see: Menu->Users)");
}

sub remove_selected_user {
    my $iter = $users_treeview->get_selection->get_selected;
    my $selected_user = $users_liststore->get($iter, 0);
    delete $users_table{lc $selected_user};
    $users_liststore->remove($iter);
}

sub save_usernames_to_file {
    open my $fh, '>', $CONFIG{youtube_users_file} or return 0;
    local $, = "\n";
    print $fh sort { lc $a cmp lc $b } values %users_table;
    close $fh;
}

# ----- My panel settings ----- #
sub log_out {
    change_subscription_page(0);

    unlink $authentication_file
      or warn "Can't unlink: `$authentication_file' -> $!";

    $yv_obj->set_access_token();
    $yv_obj->set_refresh_token();

    $statusbar->push(1, "Not logged.");
    return 1;
}

sub change_subscription_page {
    my ($value) = @_;
    foreach my $object (qw(subsc_scrollwindow subsc_label)) {
        $value
          ? $gui->get_object($object)->show
          : $gui->get_object($object)->hide;
    }
    return 1;
}

sub subscriptions_button {
    my $type     = $panel_account_type_combobox->get_active_text;
    my $username = $panel_user_entry->get_text;
    subscriptions($type, $username);
}

sub favorites_button {
    my $type     = $panel_account_type_combobox->get_active_text;
    my $username = $panel_user_entry->get_text;
    favorites($type, $username);
}

sub uploads_button {
    my $type     = $panel_account_type_combobox->get_active_text;
    my $username = $panel_user_entry->get_text;
    uploads($type, $username);
}

sub likes_button {
    my $type     = $panel_account_type_combobox->get_active_text;
    my $username = $panel_user_entry->get_text;
    likes($type, $username);
}

sub dislikes_button {
    my $type     = $panel_account_type_combobox->get_active_text;
    my $username = $panel_user_entry->get_text;
    dislikes($type, $username);
}

{
    no strict 'refs';
    foreach my $name (qw(favorites uploads likes dislikes playlists subscriptions)) {
        *{__PACKAGE__ . '::' . $name} = sub {
            my ($type, $channel) = @_;

            my $method = $type =~ /^user/i ? ($name . '_from_username') : $name;

            if (not $type =~ /^(?:user|channel)/i) {
                if ($name eq 'likes') {
                    $method = 'my_likes';
                }
            }

            if ($name eq 'dislikes') {
                $method = 'my_dislikes';
            }

            my $request = $yv_obj->$method(
                                             $type =~ /^(?:user|channel)/i && $channel =~ /^\S+\z/
                                           ? $channel
                                           : ()
                                          );

            if ($yv_utils->has_entries($request)) {
                $liststore->clear if $CONFIG{clear_search_list};
                display_results($request);
            }
            else {
                die "No $name results" . ($channel ? " for channel: <$channel>\n" : "\n");
            }

            return 1;
        };
    }
}

sub get_selected_entry_code {
    my (%options) = @_;
    my $iter = $treeview->get_selection->get_selected // return;
    if (not $options{force}) {
        return unless defined $liststore->get($iter, 4);
    }
    my $code = $liststore->get($iter, 3);
    return wantarray ? ($code, $iter) : $code;
}

# Check if keywords are actually something else
sub check_keywords {
    given ($_[0]) {
        when (/$get_video_id_re/o) {
            my $info = $yv_obj->video_details($+{video_id}, EXTRA_VIDEO_PART);

            if ($yv_utils->has_entries($info)) {
                if (not play_video($info->{results}{items}[0])) {
                    return;
                }
            }
            else {
                continue;
            }
        }
        when (/$get_playlist_id_re/o) {
            list_playlist($+{playlist_id});
        }
        when (/$get_course_id_re/) {
            display_results($yv_obj->get_video_lectures_from_course($+{course_id}));
        }
        default {
            return;
        }
    }
    return 1;
}

sub search {
    my $keywords = $search_entry->get_text();

    return if check_keywords($keywords);
    $liststore->clear if $CONFIG{clear_search_list};

    # Remember the input text when "history" is enabled
    if ($CONFIG{history}) {
        append_to_history($keywords);
    }

    if ((my $period = $published_within_combobox->get_active_text) !~ /^any/i) {
        my $amount = $spin_published_within->get_value;
        my $date = $yv_utils->period_to_date($amount, $period);
        $yv_obj->set_publishedAfter($date);
    }

    # Set the username
    my $username = $from_author_entry->get_text;
    if ($username =~ /^[\w\-]+\z/) {
        $yv_obj->set_channelId($yv_obj->channel_id_from_username($username) // $username);
    }
    else {
        $yv_obj->set_channelId();
    }

    # Set the category ID
    my $category_id = $category_id_entry->get_text;
    if ($category_id =~ /^\d+\z/) {
        $yv_obj->set_videoCategoryId($category_id);
    }
    else {
        $yv_obj->set_videoCategoryId();
    }

    my @types;
    if ($search_for_playlists_checkbox->get_active) {
        push @types, 'playlist';
    }

    if ($search_for_channels_checkbox->get_active) {
        push @types, 'channel';
    }

    if ($search_for_videos_checkbox->get_active) {
        push @types, 'video';
    }

    my $type = @types ? join(',', @types) : 'video';
    display_results($yv_obj->search_for($type, $keywords));

    return 1;
}

#---------------------- PRINT VIDEO RESULTS ----------------------#
sub encode_entities {
    my ($text) = @_;
    return q{} if not defined $text;
    $text =~ s/&/&amp;/g;
    $text =~ s/</&lt;/g;
    $text =~ s/>/&gt;/g;
    return $text;
}

sub get_next_page_spaces {
    $CONFIG{show_thumbs} ? "\t" x 10 : "\t" x 20;
}

sub get_code {
    my ($code, $iter) = get_selected_entry_code(force => 1);
    $code // return;

    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 {

        my $next_page_token = $liststore->get($iter, 5);
        my $results = $yv_obj->next_page($code, $next_page_token);

        if ($yv_utils->has_entries($results)) {
            $liststore->set($iter, 0, get_next_page_spaces() . '<big><b>' . ('=' x 20) . '</b></big>');
            $liststore->set($iter, 3, q{});
        }
        else {
            $liststore->remove($iter);
            die "This is the last page!\n";
        }

        display_results($results);
      }
      : $type eq 'video' ? play_video($yv_obj->parse_json_string($liststore->get($iter, 8)))
      :                    return;
}

sub _make_row_description {
    (my $row_description = join(q{ }, split(q{ }, $_[0]))) =~ s/(.)\1{3,}/$1/sg;
    return $row_description;
}

sub _append_next_page {
    my ($url, $token) = @_;

    $token // return;    # no next page is available

    my $iter = $liststore->append;
    $liststore->set($iter, 0, get_next_page_spaces() . "<big><b>LOAD MORE</b></big>");
    $liststore->set($iter, 3, $url);
    $liststore->set($iter, 5, $token);
    $liststore->set($iter, 7, 'next_page');
}

#
## Copy from: http://cpansearch.perl.org/src/SREZIC/Image-Info-1.38/lib/Image/Info.pm
#
sub determine_image_format {
    local ($_) = @_;
    return "JPEG" if /^\xFF\xD8/;
    return "PNG"  if /^\x89PNG\x0d\x0a\x1a\x0a/;
    return "GIF"  if /^GIF8[79]a/;
    return "TIFF" if /^MM\x00\x2a/;
    return "TIFF" if /^II\x2a\x00/;
    return "BMP"  if /^BM/;
    return "ICO"  if /^\000\000\001\000/;
    return "PPM"  if /^P[1-6]/;
    return "XPM"  if /(^\/\* XPM \*\/)|(static\s+char\s+\*\w+\[\]\s*=\s*{\s*"\d+)/;
    return "XBM"  if /^(?:\/\*.*\*\/\n)?#define\s/;
    return "SVG"  if /^(<\?xml|[\012\015\t ]*<svg\b)/;
    return undef;
}

sub _get_pixbuf_thumbnail {
    my ($url) = @_;

    my $thumbnail =
      $CONFIG{use_threads_for_thumbs}
      ? threads_lwp_get($url)
      : $yv_obj->lwp_get($url);

    my $pixbuf;
    if (defined $thumbnail) {
        my $type = determine_image_format($thumbnail);

        my $pixbufloader;
        if (defined($type)) {
            $pixbufloader = eval { 'Gtk2::Gdk::PixbufLoader'->new_with_type(lc($type)) };
        }
        if (not defined $pixbufloader) {
            $pixbufloader = 'Gtk2::Gdk::PixbufLoader'->new;
        }

        $pixbufloader->set_size(120, 90);
        $pixbufloader->write($thumbnail);
        $pixbuf = $pixbufloader->get_pixbuf;
        $pixbufloader->close;
    }
    else {
        $pixbuf = $default_thumb;
    }

    return $pixbuf;
}

sub display_results {
    my ($results) = @_;

    if (not $yv_utils->has_entries($results)) {
        die "No results...\n";
    }

    my $url   = $results->{url};
    my $info  = $results->{results} // {};
    my $items = $info->{items} // [];

    hide_feeds_window();

    my %pos;
    my $pos = 0;
    my @video_ids;
    while (my ($i, $item) = each @{$items}) {
        if ($yv_utils->is_video($item)) {
            $pos{$i} = $pos++;
            push @video_ids, $yv_utils->get_video_id($item);
        }
    }

    my $video_details;
    if (@video_ids) {
        my $content_details = $yv_obj->video_details(join(',', @video_ids), VIDEO_PART);
        $video_details = $content_details->{results}{items};
    }

    while (my ($i, $item) = each @{$items}) {
        if ($yv_utils->is_playlist($item)) {
            add_playlist_entry($item);
        }
        elsif ($yv_utils->is_channel($item)) {
            add_channel_entry($item);
        }
        elsif ($yv_utils->is_subscription($item)) {
            add_subscription_entry($item);
        }
        elsif ($yv_utils->is_video($item)) {
            @{$item}{qw(contentDetails statistics)} = @{$video_details->[$pos{$i}]}{qw(contentDetails statistics)};
            add_video_entry($item);
        }
    }

    _append_next_page($url, $info->{nextPageToken});
}

sub add_subscription_entry {
    my ($subscription) = @_;

    my $iter            = $liststore->append;
    my $row_description = $yv_utils->get_description($subscription);

    $liststore->set($iter, 4, $row_description);
    $liststore->set($iter, 7, 'subscription');
    $row_description = _make_row_description($row_description);

    $liststore->set($iter, 3, $yv_utils->get_channel_id($subscription));
    $liststore->set(
                    $iter,
                    0,
                    '<big><b>'
                      . encode_entities($yv_utils->get_title($subscription))
                      . "</b></big>\n\n"
                      . "<b>$symbols{face}\t</b> "
                      . encode_entities($yv_utils->get_channel_id($subscription)) . "\n"
                      . "<b>$symbols{crazy_arrow}\t</b> "
                      . $yv_utils->get_publication_date($subscription)
                      . "\n\n<i>"
                      . encode_entities($row_description) . '</i>'
                   );

    $liststore->set($iter, 2, "<b>$symbols{category}</b>  " . 'Subscription' . "\n");

    if ($CONFIG{show_thumbs}) {
        my $pixbuf = _get_pixbuf_thumbnail($yv_utils->get_thumbnail_url($subscription, $CONFIG{thumbnail_type}));
        $liststore->set_value($iter, 1, $pixbuf);
    }
}

sub add_video_entry {
    my ($video) = @_;

    my $iter = $liststore->append;

    my $row_description = $yv_utils->get_description($video);
    $liststore->set($iter, 4, $row_description);
    $liststore->set($iter, 6, $yv_utils->get_channel_id($video));
    $liststore->set($iter, 7, 'video');
    $liststore->set($iter, 8, $yv_obj->make_json_string($video));
    $row_description = _make_row_description($row_description);

    $liststore->set($iter, 3, $yv_utils->get_video_id($video));
    $liststore->set(
                    $iter,
                    0,
                    "<big><b>"
                      . encode_entities($yv_utils->get_title($video))
                      . "</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{face}\t</b> "
                      . encode_entities($yv_utils->get_channel_title($video)) . "\n"
                      . "<b>$symbols{ellipsis}\t</b> "
                      . encode_entities($yv_utils->get_channel_id($video)) . "\n" . "<i>"
                      . encode_entities($row_description) . "</i>"
                   );

    $liststore->set(
        $iter, 2,
        "<b>$symbols{play}\t</b> "
          . $yv_utils->format_time($yv_utils->get_duration($video)) . "\n"
          . "<b>$symbols{category}\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)
    );

    if ($CONFIG{show_thumbs}) {
        my $thumb_url = $yv_utils->get_thumbnail_url($video, $CONFIG{thumbnail_type});
        my $pixbuf = _get_pixbuf_thumbnail($thumb_url);
        $liststore->set_value($iter, 1, $pixbuf);
    }
}

sub add_channel_entry {
    my ($channel) = @_;

    my $iter            = $liststore->append;
    my $row_description = $yv_utils->get_description($channel);

    $liststore->set($iter, 4, $row_description);
    $liststore->set($iter, 7, 'channel');
    $row_description = _make_row_description($row_description);

    $liststore->set($iter, 3, $yv_utils->get_channel_id($channel));
    $liststore->set(
                    $iter,
                    0,
                    '<big><b>'
                      . encode_entities($yv_utils->get_title($channel))
                      . "</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($yv_utils->get_channel_id($channel)) . "\n"
                      . "<b>$symbols{crazy_arrow}\t</b> "
                      . $yv_utils->get_publication_date($channel)
                      . "\n\n<i>"
                      . encode_entities($row_description) . '</i>'
                   );

    $liststore->set($iter, 2, "<b>$symbols{category}</b>  " . 'Channel' . "\n");

    if ($CONFIG{show_thumbs}) {
        my $pixbuf = _get_pixbuf_thumbnail($yv_utils->get_thumbnail_url($channel, $CONFIG{thumbnail_type}));
        $liststore->set_value($iter, 1, $pixbuf);
    }
}

sub add_playlist_entry {
    my ($playlist) = @_;

    my $iter            = $liststore->append;
    my $row_description = $yv_utils->get_description($playlist);

    $liststore->set($iter, 4, $row_description);
    $row_description = _make_row_description($row_description);

    $liststore->set($iter, 6, $yv_utils->get_channel_id($playlist));
    $liststore->set($iter, 7, 'playlist');
    $liststore->set($iter, 3, $yv_utils->get_playlist_id($playlist));
    $liststore->set(
                    $iter,
                    0,
                    '<big><b>'
                      . encode_entities($yv_utils->get_title($playlist))
                      . "</b></big>\n\n"
                      . "<b>$symbols{face}\t</b> "
                      . encode_entities($yv_utils->get_channel_title($playlist)) . "\n"
                      . "<b>$symbols{play}\t</b> "
                      . encode_entities($yv_utils->get_playlist_id($playlist)) . "\n"
                      . "<b>$symbols{crazy_arrow}\t</b> "
                      . $yv_utils->format_date($yv_utils->get_publication_date($playlist)) . "\n\n" . '<i>'
                      . encode_entities($row_description) . '</i>'
                   );

    $liststore->set($iter, 2, "<b>$symbols{category}</b>  " . 'Playlist' . "\n");

    if ($CONFIG{show_thumbs}) {
        my $pixbuf = _get_pixbuf_thumbnail($yv_utils->get_thumbnail_url($playlist, $CONFIG{thumbnail_type}));
        $liststore->set_value($iter, 1, $pixbuf);
    }
}

sub print_channel_suggestions {
    my $results = $yv_obj->get_channel_suggestions();
    $liststore->clear if $CONFIG{clear_search_list};
    display_results($results);
}

sub list_playlist {
    my ($playlist_id) = @_;

    my $results = $yv_obj->videos_from_playlist_id($playlist_id);
    if ($yv_utils->has_entries($results)) {
        $liststore->clear if $CONFIG{clear_search_list};
        display_results($results);
        return 1;
    }
    else {
        die "[!] Inexistent playlist...\n";
    }
    return;
}

# Get playlists from username
sub playlists_from_selected_username {
    my $iter = $users_treeview->get_selection->get_selected;
    playlists('username', $users_liststore->get($iter, 0));
}

sub videos_from_selected_username {
    my $iter = $users_treeview->get_selection->get_selected;
    my $username = $users_liststore->get($iter, 0);
    uploads('user', $username);
}

sub get_username_from_list {
    hide_users_list_window();
    videos_from_selected_username();
}

sub favorites_from_text_entry {
    my ($text_entry) = @_;
    favorites($channel_type_combobox->get_active_text, $text_entry->get_text);
}

sub uploads_from_text_entry {
    my ($text_entry) = @_;
    uploads($channel_type_combobox->get_active_text, $text_entry->get_text);
}

sub playlists_from_text_entry {
    my ($text_entry) = @_;
    playlists($channel_type_combobox->get_active_text, $text_entry->get_text);
}

sub likes_from_text_entry {
    my ($text_entry) = @_;
    likes($channel_type_combobox->get_active_text, $text_entry->get_text);
}

sub subscriptions_from_text_entry {
    my ($text_entry) = @_;
    subscriptions($channel_type_combobox->get_active_text, $text_entry->get_text);
}

# Get videos from username
sub videos_from_username {
    my ($username) = @_;
    is_valid_username($username) or return;

    my $results = $yv_obj->get_videos_from_username($username);
    if ($yv_utils->has_entries($results)) {
        $liststore->clear if $CONFIG{clear_search_list};
        display_results($results);
    }
    else {
        die "No video uploaded by: <$username>\n";
    }
    return 1;
}

sub is_valid_username {
    my ($username) = @_;
    die "Invalid username: <$username>\n"
      unless $username =~ /$valid_username_re/;
    return 1;
}

sub _strip_edge_spaces {
    my ($text) = @_;
    $text =~ s/^\s+//;
    return unpack 'A*', $text;
}

sub get_streaming_url {
    my ($video_id) = @_;

    my @info = $yv_obj->get_streaming_urls($video_id);

    my (@urls, @captions);
    foreach my $entry (@info) {
        if (exists $entry->{url} and exists $entry->{itag}) {
            push @urls, $entry;
        }
        elsif (exists $entry->{lc} and exists $entry->{u}) {
            push @captions, $entry;
        }
    }

    # Download the closed-captions
    my $srt_file;
    if (@captions and $CONFIG{get_captions}) {
        state $x = require WWW::YoutubeViewer::GetCaption;
        my $yv_cap = WWW::YoutubeViewer::GetCaption->new(
                                                         captions_dir => $CONFIG{captions_dir},
                                                         captions     => \@captions,
                                                         languages    => $CONFIG{srt_languages},
                                                        );
        $srt_file = $yv_cap->save_caption($video_id);
    }

    state $x        = require WWW::YoutubeViewer::Itags;
    state $yv_itags = WWW::YoutubeViewer::Itags->new();

    my ($streaming, $resolution) =
      $yv_itags->find_streaming_url(
                                    urls           => \@urls,
                                    resolution     => $CONFIG{resolution},
                                    dash           => $CONFIG{dash_support},
                                    dash_mp4_audio => $CONFIG{dash_mp4_audio},
                                   );

    my $info = {};
    if (not @urls) {
        %{$info} = map { ref($_) eq 'HASH' ? %{$_} : () } @info;
    }

    return {
            streaming  => $streaming,
            srt_file   => $srt_file,
            info       => $info,
            resolution => $resolution,
           };
}

sub get_quotewords {
    state $x = require Text::ParseWords;
    return Text::ParseWords::quotewords(@_);
}

#---------------------- PLAY AN YOUTUBE VIDEO ----------------------#
sub get_player_command {
    my ($streaming, $video) = @_;

    my %MPLAYER;
    $MPLAYER{fullscreen} = $CONFIG{fullscreen} ? $CONFIG{video_players}{$CONFIG{video_player_selected}}{fs} : q{};
    $MPLAYER{mplayer_arguments} = $CONFIG{video_players}{$CONFIG{video_player_selected}}{arg} // q{};

    my $cmd = join(
        q{ },
        (
            # Video player
            $CONFIG{video_players}{$CONFIG{video_player_selected}}{cmd},

            (    # Audio file (http://)
               ref($streaming->{streaming}{__AUDIO__}) eq 'HASH'
                 && exists($CONFIG{video_players}{$CONFIG{video_player_selected}}{audio})
               ? $CONFIG{video_players}{$CONFIG{video_player_selected}}{audio}
               : ()
            ),

            (    # Caption file (.srt)
               defined($streaming->{srt_file})
                 && exists($CONFIG{video_players}{$CONFIG{video_player_selected}}{srt})
               ? $CONFIG{video_players}{$CONFIG{video_player_selected}}{srt}
               : ()
            ),

            # Rest of the arguments
            grep({ defined($_) and /\S/ } values %MPLAYER)
        )
    );

    my $has_video = $cmd =~ /\*(?:VIDEO|URL|ID)\*/;
    $cmd = $yv_utils->format_text($streaming, $video, $cmd, 1);
    $has_video ? $cmd : join(' ', $cmd, quotemeta($streaming->{streaming}{url}));
}

sub play_video {
    my ($video) = @_;

    my $streaming = get_streaming_url($yv_utils->get_video_id($video));

    if (defined $streaming->{info}{status} and $streaming->{info}{status} =~ /\bfail/i) {
        die "[x_x] Error on: " . sprintf($CONFIG{youtube_video_url}, $video->{videoID}) . "\n",
          "[x_x] Reason: " . $streaming->{info}{reason} =~ s/\+/ /gr . "\n";
    }

    if (ref($streaming->{streaming}) ne 'HASH') {
        warn "Can't play this video: no streaming data has been found!\n";
        return;
    }

    my $command = get_player_command($streaming, $video);

    if ($yv_obj->get_debug) {
        say "-> Resolution: $streaming->{resolution}";
        say "-> Video itag: $streaming->{streaming}{itag}";
        say "-> Audio itag: $streaming->{streaming}{__AUDIO__}{itag}" if exists $streaming->{streaming}{__AUDIO__};
        say "-> Video type: $streaming->{streaming}{type}";
        say "-> Audio type: $streaming->{streaming}{__AUDIO__}{type}" if exists $streaming->{streaming}{__AUDIO__};
    }

    my $code = execute_external_program($command);
    warn "Can't play this video -- player exited with code: $code\n" if $code != 0;

    return 1;
}

sub list_category {
    my $iter   = $cat_treeview->get_selection->get_selected;
    my $cat_id = $cats_liststore->get($iter, 1) // return;
    my $type   = $cats_liststore->get($iter, 3);

    my $videos =
        $type eq 'edu-cat'
      ? $yv_obj->get_video_lectures_from_category($cat_id)
      : $yv_obj->videos_from_category($cat_id);

    if ($yv_utils->has_entries($videos)) {
        $liststore->clear if $CONFIG{clear_search_list};
        display_results($videos);
    }
    else {
        die "No video found for categoryID: <$cat_id>\n";
    }
}

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);

    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;
    }

    $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)
                   );
}

sub clear_text {
    $_[0]->set_text('') if $CONFIG{clean_text_entries_on_click};
    return 0;
}

sub run_cli_youtube_viewer {
    execute_cli_youtube_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{},
                  );

    while (my ($argv, $value) = each %options) {
        push(
            @args,
            do {
                $value             ? '--' . $argv . '=' . $value
                  : defined $value ? '--' . $argv
                  :                  next;
              }
            );
    }
    return @args;
}

sub execute_external_program {
    my ($cmd) = @_;

    my $pid = fork();
    if (defined $pid) {
        if ($pid == 0) {
            say "** Forking process: $cmd" if $yv_obj->get_debug;
            $yv_obj->proxy_exec($cmd);
        }
    }
    else {
        say "** Backgrounding process: $cmd" if $yv_obj->get_debug;
        $yv_obj->proxy_system($cmd . ' &');
    }
}

sub _make_youtube_url {
    my ($code, $type) = @_;

    my $format = (
                    ($type eq 'subscription' || $type eq 'channel') ? $CONFIG{youtube_channel_url}
                  : $type eq 'video'    ? $CONFIG{youtube_video_url}
                  : $type eq 'playlist' ? $CONFIG{youtube_playlist_url}
                  :                       ()
                 );

    if (defined $format) {
        return sprintf($format, $code);
    }

    return "https://www.youtube.com";
}

sub open_youtube_url {
    my ($code, $iter) = get_selected_entry_code();
    $code // return;

    my $type = $liststore->get($iter, 7);
    my $url = _make_youtube_url($code, $type);

    my $exit_code =
      execute_external_program(join(q{ }, $CONFIG{web_browser} // $ENV{WEBBROWSER} // 'xdg-open', quotemeta($url)));
    warn "Can't open YouTube URL - exit code: $exit_code\n" if $exit_code != 0;
    return 1;
}

my @queue_codes;

sub queue_playback {
    my $code = get_selected_entry_code() // return;
    print "[*] Added: <$code>\n" if $yv_obj->get_debug;
    push @queue_codes, $code;
    return 1;
}

sub play_videos_from_queue {
    if (@queue_codes) {
        execute_cli_youtube_viewer('--video-ids=' . join(q{,}, splice @queue_codes));
    }
    return 1;
}

sub play_all_video_results {
    my $model = $treeview->get_model;
    my $iter = $model->get_iter_first // return;

    my @ids;

    do {
        push @ids, $liststore->get($iter, 3);
    } while defined($iter = $model->iter_next($iter));

    execute_cli_youtube_viewer('--video-ids=' . join(q{,}, grep { /$valid_video_id_re/ } @ids));

    return 1;
}

sub play_selected_video_with_cli_youtube_viewer {
    my $code = get_selected_entry_code() // return;

    if ($code =~ /$valid_video_id_re/) {
        execute_cli_youtube_viewer("--video-id=$code");
    }
    elsif ($code =~ /$valid_playlist_id_re/) {
        execute_cli_youtube_viewer("--pp=$code");
    }
    else {
        warn "Can't play: $code\n";
    }

    return 1;
}

sub execute_cli_youtube_viewer {
    my @arguments = @_;

    my $command = join(
                       q{ },
                       $CONFIG{terminal},
                       sprintf(
                               $CONFIG{terminal_exec},
                               join(q{ }, $CONFIG{youtube_viewer}, get_options_as_arguments(), @arguments)
                              )
                      );
    my $code = execute_external_program($command);

    say $command if $yv_obj->get_debug;

    warn "youtube-viewer - exit code: $code\n" if $code != 0;
    return 1;
}

sub download_video {
    my ($code, $iter) = get_selected_entry_code();
    $code // return;

    my $type = $liststore->get($iter, 7);
    if ($type ne 'video') {
        warn "Can't download resource: $type\n";
        return;
    }

    execute_cli_youtube_viewer("--video-id=$code", '--download');
    return 1;
}

sub comments_row_activated {
    my $iter = $feeds_treeview->get_selection->get_selected() or return;
    my $value = $feeds_liststore->get($iter, 1);

    if (defined $value and $value =~ m{^https?://}) {
        $feeds_liststore->remove($iter);
        my ($url, $token) = split(/;/, $value);
        my $results = $yv_obj->next_page($url, $token);
        if ($yv_utils->has_entries($results)) {
            display_comments($results);
        }
        else {
            die "This is the last page of comments.\n";
        }
    }

    return 1;
}

sub get_user_favorited_videos {
    my $username = get_channel_id_for_selected_video() // return;
    favorites('channel', $username);
}

sub get_channel_id_for_selected_video {
    my $iter = $treeview->get_selection->get_selected() or return;
    return $liststore->get($iter, 6);
}

sub show_related_videos {
    my $code = get_selected_entry_code() // return;

    my $results = $yv_obj->related_to_videoID($code);
    if ($yv_utils->has_entries($results)) {
        $liststore->clear if $CONFIG{clear_search_list};
        display_results($results);
    }
    else {
        die "No related video for videoID: <$code>\n";
    }
}

sub favorite_video {
    my $code = get_selected_entry_code() // return;

    $feeds_statusbar->push(
                           0, $yv_obj->favorite_video($code)
                           ? 'Video favorited.'
                           : 'Error!'
                          );
}

sub subscribe_channel {
    my $channel_id = get_channel_id_for_selected_video();
    $feeds_statusbar->push(0,
                           $yv_obj->subscribe_channel($channel_id)
                           ? "Successfully subscribed to channel: $channel_id."
                           : 'Error!');
}

sub like_selected_video {
    my $code = get_selected_entry_code() // return;
    $feeds_statusbar->push(
                           0, $yv_obj->send_rating_to_video($code, 'like')
                           ? 'Video liked.'
                           : 'Error!'
                          );
}

sub dislike_selected_video {
    my $code = get_selected_entry_code() // return;
    $feeds_statusbar->push(
                           0, $yv_obj->send_rating_to_video($code, 'dislike')
                           ? 'Video disliked.'
                           : 'Error!'
                          );
}

sub send_comment_to_video {
    my $videoID = get_selected_entry_code() // return;
    my $comment = get_text($gui->get_object('comment_textview'));

    $feeds_statusbar->push(0,
                           length($comment) && $yv_obj->comment_to_video_id($comment, $videoID)
                           ? 'Video comment has been posted!'
                           : 'Error!');
}

sub display_comments {
    my ($results) = @_;

    my $url      = $results->{url};
    my $res      = $results->{results} // {};
    my $comments = $res->{items} // [];

    foreach my $comment (@{$comments}) {
        my $snippet = (($comment->{snippet} // next)->{topLevelComment} // next)->{snippet};

        my $iter = $feeds_liststore->append;
        $feeds_liststore->set(
                              $iter,
                              0,
                              "<big><b>"
                                . encode_entities($snippet->{authorDisplayName})
                                . "</b> ("
                                . $yv_utils->format_date($snippet->{publishedAt})
                                . ") said:</big>\n\t"
                                . encode_entities($snippet->{textDisplay} // 'Empty comment...')
                             );
    }

    if (exists $res->{nextPageToken}) {
        my $iter = $feeds_liststore->append;
        $feeds_liststore->set($iter, 0, "\n<big><b>LOAD MORE...</b></big>\n");
        $feeds_liststore->set($iter, 1, "$url;$res->{nextPageToken}");
    }

    return 1;
}

sub show_more_videos_from_username {
    uploads('channel', get_channel_id_for_selected_video() || return);
}

sub show_playlists_from_username {
    my $request = $yv_obj->playlists(get_channel_id_for_selected_video() || return);
    if ($yv_utils->has_entries($request)) {
        $liststore->clear if $CONFIG{clear_search_list};
        display_results($request);
    }
    else {
        die "No playlists found...\n";
    }
    return 1;
}

sub set_entry_details {
    my ($code, $iter) = @_;

    my $type         = $liststore->get($iter, 7);
    my $main_details = $liststore->get($iter, 0);

    # Setting title
    my $title = substr($main_details, 0, index($main_details, '</big>') + 6, '');
    $gui->get_object('video_title_label')->set_label("<big>$title</big>");

    # 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;

    my $secondary_details = $liststore->get($iter, 2);
    $secondary_details =~ s{\h+}{ }g;
    $secondary_details =~ s{^<b>.*?</b>\K\h*}{\t}gm;

    $gui->get_object('video_details_label')->set_label($main_details . $secondary_details);

    # Setting the link button
    my $url = _make_youtube_url($code, $type);
    my $linkbutton = $gui->get_object('linkbutton1');
    $linkbutton->set_label($url);
    $linkbutton->set_uri($url);

    # Getting thumbs
    foreach my $nr (qw(1 2 3)) {
        if ($code =~ /$valid_video_id_re/) {
            my $pixbuf = _get_pixbuf_thumbnail(sprintf($CONFIG{youtube_thumb_url}, $code, $nr));
            $gui->get_object("image$nr")->set_from_pixbuf($pixbuf);
        }
        else {
            $gui->get_object("image$nr")->set_from_pixbuf($default_thumb);
        }
    }

    # Setting textview description
    set_text($gui->get_object('description_textview'), $liststore->get($iter, 4));
    return 1;
}

sub on_mainw_destroy {

    # Save hpaned position
    $CONFIG{hpaned_position} = $hbox2->get_position;

    get_main_window_size();
    dump_configuration();
    save_usernames_to_file();

    'Gtk2'->main_quit;
}

$notebook->set_current_page($CONFIG{default_notebook_page});

'Gtk2'->main;
