#!/usr/bin/perl 

eval 'exec /usr/bin/perl  -S $0 ${1+"$@"}'
    if 0; # not running under some shell

# Copyright (C) 2010-2013 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: 06 September 2013
#  Website: http://trizen.googlecode.com
#-------------------------------------------------------

use 5.010;
use strict;

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

#use lib qw(../lib);    # devel only
#use warnings;          # debug only

use Gtk2 qw(-init);
use autouse 'File::Basename' => qw(dirname);
use File::Spec::Functions qw(
  rel2abs
  catdir catfile
  curdir updir
  path tmpdir
  );

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

# Developer key
my $key = 'eTj9NtCyOsGMliTTwz-T85muGT-ARAwVREslfB_giHP3X339Jkpn5Xf71pQXY96xWtFY1oFHt530ct5uZZJk5YTghbNm2IrwZ4';

# Configuration dir/file
my $xdg_config_home = $ENV{XDG_CONFIG_HOME}
  || catdir(($ENV{HOME} || $ENV{LOGDIR} || (getpwuid($<))[7] || `echo -n ~`), '.config');

# Configuration dir/file
my $config_dir = catdir($xdg_config_home, $execname);
my $config_file = catfile($config_dir, "$execname.conf");

if (not -d $config_dir) {
    require File::Path;
    File::Path::make_path($config_dir)
      or warn "[!] Can't create dir '$config_dir': $!";
}

# Unchangeable variables goes here
my %constant = (win32 => $^O =~ /^mswin\d/i || 0,);

# CLI Youtube Viewer
my $cli_youtube_viewer = '/usr/share/gtk-youtube-viewer/youtube-viewer';

if (not -e $cli_youtube_viewer) {
    $cli_youtube_viewer = rel2abs('./youtube-viewer');
}

if (not -e $cli_youtube_viewer) {
    require File::Basename;
    my $dir = File::Basename::dirname(rel2abs($0));
    $cli_youtube_viewer = catfile($dir, 'youtube-viewer');
}

# Locating gcap
my $gcap;
foreach my $path (path()) {
    if (-e (my $gcap_path = catfile($path, 'gcap'))) {
        $gcap = $gcap_path;
        last;
    }
}

# Get mplayer
sub get_mplayer {
    if ($constant{win32}) {
        my $smplayer = catfile($ENV{ProgramFiles}, qw(SMPlayer mplayer mplayer.exe));

        if (not -e $smplayer) {
            warn "\n\n!!! Please install SMPlayer in order to stream YouTube videos.\n\n";
        }

        return $smplayer;    # Windows MPlayer
    }
    else {
        my $mplayer_path = '/usr/bin/mplayer';
        return -x $mplayer_path ? $mplayer_path : q{mplayer}    # *NIX MPlayer
    }
}

# Main configuration
my %CONFIG = (

    # Combobox values
    active_sort_by_combobox      => 0,
    active_time_sort_by_combobox => 0,
    active_resolution_combobox   => 0,
    active_search_for            => 0,
    active_hd_combobox           => 0,
    active_caption_combobox      => 0,
    active_duration_combobox     => 0,
    active_safe_search_combobox  => 1,
    active_more_options_expander => 0,

    # MPlayer options
    cache             => 30000,
    cache_min         => 5,
    lower_cache       => 1000,
    lower_cache_min   => 3,
    mplayer           => get_mplayer(),
    mplayer_srt_args  => '-sub %s',
    mplayer_arguments => '-prefer-ipv4 -really-quiet -cache %d -cache-min %d',

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

    # Youtube options
    prefer_webm         => 1,
    prefer_https        => 0,
    results             => 10,
    resolution          => 'original',
    hd                  => undef,
    safe_search         => undef,
    caption             => undef,
    duration            => undef,
    time                => undef,
    orderby             => undef,
    categories_language => 'en-US',

    # URI options
    thumb_url           => 'http://i.ytimg.com/vi/%s/%s.jpg',
    youtube_video_url   => 'http://www.youtube.com/watch?v=%s',
    playlists_url       => 'http://www.youtube.com/playlist?list=%s',
    youtube_profile_url => 'http://www.youtube.com/user/%s',

    # Subtitle options
    srt_languages => ['en', 'jp'],
    captions_dir  => tmpdir(),
    get_captions  => 1,
    gcap          => $gcap,

    # Others
    http_proxy      => undef,
    page            => 1,
    debug           => 0,
    fullscreen      => 0,
    use_lower_cache => 0,
    use_threads     => do {
        eval { require threads };
        $@ ? 0 : 1;
    },
    use_threads_for_thumbs => 0,                                           # not very stable
    hpaned_position        => 420,
    search_channels        => 0,
    search_playlists       => 0,
    access_token           => undef,
    refresh_token          => undef,
    thousand_separator     => q{,},
    perl_bin               => $^X,
    default_thumb          => 'default',
    downloads_folder       => $ENV{'HOME'},
    browser                => undef,
    terminal               => find_terminal(),
    cli_youtube_viewer     => $cli_youtube_viewer,
    users_list             => catfile($config_dir, 'youtube_users.txt'),
);

my %MPLAYER;

# MPlayer variable arguments
sub set_mplayer_arguments {
    my ($cache, $cache_min) = @_;
    $MPLAYER{mplayer_arguments} = sprintf $CONFIG{mplayer_arguments}, $cache, $cache_min;
    $MPLAYER{fullscreen} = $CONFIG{fullscreen} ? q{-fs} : q{};
    return 1;
}

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

# $appname - configuration file

EOD

    # Save hash config to file
    sub dump_configuration {
        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::dump(\%CONFIG);
        print $config_fh $config_documentation, $dumped_config;
        close $config_fh;
    }
}

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

# SIG handlers
foreach my $sig (qw(HUP TERM INT KILL)) {
    $SIG{$sig} = \&on_mainw_destroy;
}

# Locating the .glade interface file and icons dir
my ($interface_path, $icons_path);
if (-f (my $glade_file = catfile(updir(), 'share', "$execname.glade"))) {
    $interface_path = rel2abs($glade_file);
    $icons_path = catdir(dirname($interface_path), 'gtk-youtube-viewer-icons');
}
else {
    foreach my $usr_dir (qw(/usr /usr/local . ..)) {
        if (-e ($interface_path = catfile($usr_dir, 'share', $execname, "$execname.glade"))) {
            $icons_path = catdir($usr_dir, 'share', $execname, 'icons');
            last;
        }
    }
}

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

# -------------  Get GUI objects ------------- #
my (
    $mainw,               $users_list_window,     $help_window,          $prefernces_window,
    $login_to_youtube,    $errors_window,         $details_window,       $about_window,
    $treeview,            $liststore,             $config_view,          $statusbar,
    $thumbs_column,       $textview_help,         $sort_combobox,        $sort_by_time_combobox,
    $resolution_combobox, $spin_results,          $users_treeview,       $users_liststore,
    $thumbs_checkbutton,  $search_for_combobox,   $spin_start_with_page, $fullscreen_checkbutton,
    $notebook,            $search_entry,          $from_author_entry,    $gif_spinner,
    $hbox2,               $feeds_window,          $feeds_treeview,       $feeds_liststore,
    $feeds_statusbar,     $safesearch_combobox,   $hd_combobox,          $caption_combobox,
    $duration_combobox,   $warnings_window,       $warnings_textview,    $errors_textview,
    $category_id_entry,   $more_options_expander, $cat_treeview,         $cats_liststore,
   );

sub get_objects {
    my %objects = (

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

        # Others
        'treeview1'              => \$users_treeview,
        'feeds_statusbar'        => \$feeds_statusbar,
        'treeview2'              => \$treeview,
        'treeview3'              => \$cat_treeview,
        'feeds_treeview'         => \$feeds_treeview,
        'liststore1'             => \$liststore,
        'liststore2'             => \$users_liststore,
        'liststore4'             => \$cats_liststore,
        'liststore11'            => \$feeds_liststore,
        'textview3'              => \$config_view,
        'warnings_textview'      => \$warnings_textview,
        'errors_textview'        => \$errors_textview,
        'search_entry'           => \$search_entry,
        'statusbar1'             => \$statusbar,
        'treeviewcolumn2'        => \$thumbs_column,
        'textview2'              => \$textview_help,
        'from_author_entry'      => \$from_author_entry,
        'category_id_entry'      => \$category_id_entry,
        'more_options_expander'  => \$more_options_expander,
        'notebook1'              => \$notebook,
        'combobox1'              => \$sort_combobox,
        'combobox2'              => \$sort_by_time_combobox,
        'combobox3'              => \$resolution_combobox,
        'combobox4'              => \$duration_combobox,
        'combobox5'              => \$caption_combobox,
        'combobox6'              => \$hd_combobox,
        'combobox7'              => \$safesearch_combobox,
        'spinbutton1'            => \$spin_results,
        'spinbutton2'            => \$spin_start_with_page,
        'thumbs_checkbutton'     => \$thumbs_checkbutton,
        'search_for_combobox'    => \$search_for_combobox,
        'fullscreen_checkbutton' => \$fullscreen_checkbutton,
        'gif_spinner'            => \$gif_spinner,
        'hbox2'                  => \$hbox2,
    );

    while (my ($key, $value) = each %objects) {
        ${$value} = $gui->get_object($key);
    }
}
get_objects();

# __WARN__ handle
local $SIG{__WARN__} = sub {
    my $warning = _strip_edge_spaces(join('', @_));
    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 {
    return if not [caller]->[0] ~~ [qw(main WWW::YoutubeViewer)];
    my $error = join('', @_);
    set_text(
        $errors_textview,
        $error . do {
            if ($error =~ m{^Can't locate (.+?)\.pm\b}) {
                my $module = $1;
                $module =~ s{[/\\]+}{::}g;
"\nThe module $module is required!\n\nTo install the module, 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("$icons_path/$execname.png");
my $user_icon_pixbuf   = 'Gtk2::Gdk::Pixbuf'->new_from_file_at_size("$icons_path/user.png", 16, 16);
my $feed_icon_pixbuf   = 'Gtk2::Gdk::Pixbuf'->new_from_file_at_size("$icons_path/feed_icon.png", 16, 16);
my $default_thumb      = 'Gtk2::Gdk::Pixbuf'->new_from_file_at_size("$icons_path/default_thumb.jpg", 120, 90);
my $donate_icon_pixbuf = 'Gtk2::Gdk::Pixbuf'->new_from_file("$icons_path/donate.png");
my $animation          = 'Gtk2::Gdk::PixbufAnimation'->new_from_file("$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;

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

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

if (ref $CONFIG ne 'HASH') {
    $CONFIG = do($config_file) || warn "Can't load the configuration file: $!";
}

my @valid_keys = grep exists $CONFIG{$_}, keys %{$CONFIG};
@CONFIG{@valid_keys} = @{$CONFIG}{@valid_keys};

if (ref $CONFIG ne 'HASH' or not %CONFIG ~~ %{$CONFIG}) {
    dump_configuration();
}

require WWW::YoutubeViewer;
my $yv_obj = WWW::YoutubeViewer->new(
                                     key         => $key,
                                     app_name    => $appname,
                                     app_version => $version,
                                     config_dir  => $config_dir,
                                     escape_utf8 => 1,
                                    );

{
    my $client_id     = '991455593101.apps.googleusercontent.com';
    my $client_secret = 'YcxxWCCbBwIr-IhUDCanrp41';
    my $redirect_uri  = 'urn:ietf:wg:oauth:2.0:oob';

    $yv_obj->set_client_id($client_id);
    $yv_obj->set_client_secret($client_secret);
    $yv_obj->set_redirect_uri($redirect_uri);
}

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

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

    # Menu items
    $fullscreen_checkbutton->set_active($CONFIG{fullscreen});

    # Others
    foreach my $option_name (
                             qw(
                             caption results duration
                             author orderby region category
                             categories_language safe_search
                             page debug time prefer_https 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{results});

    # Spin button start with page
    $spin_start_with_page->set_value($CONFIG{page});

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

    # Auto-login
    if (length $CONFIG{access_token}) {
        if ($yv_obj->set_access_token($CONFIG{access_token})) {
            $yv_obj->set_refresh_token($CONFIG{refresh_token});
            show_user_panel();
        }
        else {
            warn "[!] Invalid access token!\n";
        }
    }
    else {
        $statusbar->push(1, 'Not logged');
    }

    # 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
    $sort_combobox->set_active($CONFIG{active_sort_by_combobox});
    $sort_by_time_combobox->set_active($CONFIG{active_time_sort_by_combobox});
    $resolution_combobox->set_active($CONFIG{active_resolution_combobox});
    $search_for_combobox->set_active($CONFIG{active_search_for});
    $hd_combobox->set_active($CONFIG{active_hd_combobox});
    $caption_combobox->set_active($CONFIG{active_caption_combobox});
    $duration_combobox->set_active($CONFIG{active_duration_combobox});
    $safesearch_combobox->set_active($CONFIG{active_safe_search_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();
    }

    # 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($_) => $_ } (
                                    'AtGoogleTalks',    'AtheistsAreSkeptics', 'Best0fScience',        'Google',
                                    'GoogleChrome',     'GoogleTechTalks',     'KhanAcademy',          'NASAtelevision',
                                    'NatCen4ScienceEd', 'GoogleDevelopers',    'VSauce',               'Gotbletu',
                                    'ScienceChannel',   'SpaceRip',            'TEDtalksDirector',     'ThunderF00t',
                                    'MIT',              'SixtySymbols',        'SpitzerScienceCenter', 'SciShow',
                                    'UCBerkeley',       'UCTelevision',        'FOSDEMtalks',          'BigThink',
                                    'ProfessorFink',    'StanfordUniversity',  'UCTVSeminars',         '1Veritasium',
                                       );
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('channels_button')->set_image(new_image_from_pixbuf('icon_from_pixbuf', $user_icon_pixbuf));
    $gui->get_object('donate_button')->set_image(new_image_from_pixbuf('icon_from_pixbuf', $donate_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: http://trizen.googlecode.com
    development website: https://github.com/trizen/$execname
    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{users_list}

* 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

* Video tops

Valid categories are:
    @{[@{WWW::YoutubeViewer::categories_IDs}]}

Valid region IDs are:
    @{[@{WWW::YoutubeViewer::region_IDs}]}

* Configuration

use_threads
    1 to use threads when getting the XML content

use_threads_for_thumbs
    1 to use threads when getting the thumbnails

cache & cache_min
    this cache values are used for resolutions higher than 480p

lower_cache & lower_cache_min
    this cache values are used for resolutions lower than 720p

use_lower_cache
    1 to always use the lower_cache (for slow connections)

show_thumbs
    1 to show YouTube video thumbnails (may be slow)

clear_list
    1 to clean the treeview list before each search

gcap
    path to gcap application ( see http://gcap.googlecode.com )

srt_languages
    default subtitle languages (when gcap is available)

captions_dir
    is used 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 $json_data = $yv_obj->oauth_login($code) // return;

            say $json_data if $yv_obj->get_debug;
            my $info = $yv_utils->basic_json_parser($json_data);

            if ($gui->get_object('login_check_button')->get_active) {
                $CONFIG{access_token}  = $info->{access_token}  // return;
                $CONFIG{refresh_token} = $info->{refresh_token} // return;
                dump_configuration();
            }

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

            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_video_code() or return;

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

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

sub set_comments {
    my $videoID = get_selected_video_code() or return;

    return unless $videoID =~ /$valid_video_id_re/;

    $feeds_liststore->clear;
    print_comments($yv_obj->get_video_comments($videoID));
}

# Feeds window
sub show_feeds_window {
    my $videoID = get_selected_video_code() or return;

    return unless $videoID =~ /$valid_video_id_re/;

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

    print_comments($yv_obj->get_video_comments($videoID));

    return 1;
}

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

# Preferences window
sub show_preferences_window {
    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;
    die $@ if $@;

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

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

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

# Combo boxes changes
sub combobox_sort_by_changed {
    $CONFIG{active_sort_by_combobox} = $sort_combobox->get_active;
    $yv_obj->set_orderby($CONFIG{orderby} = $sort_combobox->get_active_text);
}

sub combobox_time_sort_by_changed {
    $CONFIG{active_time_sort_by_combobox} = $sort_by_time_combobox->get_active;
    $yv_obj->set_time($CONFIG{time_sort} = $sort_by_time_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_search_for_changed {
    state $labels = [qw(videos playlists channels disco_videos)];
    $CONFIG{active_search_for} = $search_for_combobox->get_active;
    $CONFIG{"search_$_"} = 0 for @{$labels};
    $CONFIG{"search_" . $search_for_combobox->get_active_text} = 1;
}

sub combobox_safesearch_changed {
    $CONFIG{active_safe_search_combobox} = $safesearch_combobox->get_active;
    $yv_obj->set_safe_search($CONFIG{safe_search} = $safesearch_combobox->get_active_text);
}

sub combobox_duration_changed {
    my $text = $duration_combobox->get_active_text;
    $CONFIG{active_duration_combobox} = $duration_combobox->get_active;
    $yv_obj->set_duration($CONFIG{duration} = $text eq 'default' ? undef : $text);
}

sub combobox_caption_changed {
    my $text = $caption_combobox->get_active_text;
    $CONFIG{active_caption_combobox} = $caption_combobox->get_active;
    $yv_obj->set_caption($CONFIG{only_videos_with_caption} = $text eq 'default' ? undef : $text);
}

sub combobox_hd_changed {
    my $text = $hd_combobox->get_active_text;
    $CONFIG{active_hd_combobox} = $hd_combobox->get_active;
    $yv_obj->set_hd($CONFIG{only_hd_videos} = $text eq 'default' ? undef : $text);
}

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

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

# Menu items
sub toggled_mplayer_fullscreen {
    $CONFIG{fullscreen} = $fullscreen_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) = @_;

    return if ref $categories ne 'ARRAY';

    foreach my $category (@{$categories}) {
        my $label = $category->{label};
        my $term  = $category->{term};

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

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

{
    # Standard categories:
    add_category_header("Categories");
    append_categories($yv_obj->get_categories(), 'cat');
    append_categories([{label => 'Movies', term => 'Movies'}, {label => 'Trailers', term => 'Trailers'}], '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) = @_;

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

    my $i = 0;
    foreach my $top_name (@{WWW::YoutubeViewer::feeds_IDs}) {

        my $top_time_copy = $top_time;
        if (++$i ~~ [3, 5, 8, 9]) {    # doesn't support the 'time' option
            $top_time_copy = $top_time eq 'today' ? q{} : next;
        }

        add_top_row($top_name, $top_time_copy);
    }
}

set_youtube_tops('today',    'Today tops');
set_youtube_tops('all_time', 'All time tops');

{
    my $iter = $tops_liststore->append;
    $tops_liststore->set($iter, 0, "<big><b>\tMovies</b></big>");
    foreach my $movie_top (@{WWW::YoutubeViewer::movie_IDs}) {
        add_top_row($movie_top, 'movies');
    }
}

# ------------ Usernames list window ------------ #
sub set_usernames {
    if (-e $CONFIG{users_list}) {
        open my $fh, '<', $CONFIG{users_list};
        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_username_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{users_list} 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);

    undef $CONFIG{access_token};
    undef $CONFIG{refresh_token};

    dump_configuration();

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

{
    no strict 'refs';
    foreach my $feed_name (@{WWW::YoutubeViewer::feed_methods}) {
        *{__PACKAGE__ . '::' . $feed_name} = sub {
            my $user   = (@_ && !ref $_[0]) ? shift() : $gui->get_object('news_users')->get_text;
            my $code   = \&{"WWW::YoutubeViewer::get_$feed_name"};
            my $videos = $yv_obj->$code($user);

            if (defined $videos) {
                $liststore->clear if $CONFIG{clear_list};
                print_videos($videos);
            }
        };
    }
}

sub get_selected_video_code {
    my (%options) = @_;
    my $iter = $treeview->get_selection->get_selected or return;
    unless ($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->get_video_info($+{video_id});
            play_videos($info->{results});
        }
        when (/$get_playlist_id_re/o) {
            list_playlist($+{playlist_id});
        }
        when (/$get_course_id_re/) {
            print_videos($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_list};
    $yv_obj->set_author($from_author_entry->get_text);
    $yv_obj->set_category($category_id_entry->get_text);

    if ($CONFIG{search_playlists}) {
        print_playlists($yv_obj->search_for_playlists($keywords));
    }
    elsif ($CONFIG{search_channels}) {
        print_channels($yv_obj->search_channels($keywords));
    }
    elsif ($CONFIG{search_disco_videos}) {
        if (defined(my $videos = $yv_obj->get_disco_videos([$keywords]))) {
            print_videos($videos);
        }
        else {
            die "[!] No disco video results!\n";
        }
    }
    else {
        print_videos($yv_obj->search($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 decode_entities {
    require HTML::Entities;
    return HTML::Entities::decode_entities($_[0]);
}

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

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

    $code =~ /$valid_playlist_id_re/
      ? do { list_playlist($code) }
      : $code =~ m{^https?://} ? do {

        my $search_type = $liststore->get($iter, 5);

        if ($yv_obj->get_debug) {
            say "** SEARCH TYPE: $search_type" if defined $search_type;
        }

        my %args;
        if (defined $search_type and $search_type ne q{}) {
            $args{$search_type} = 1;
        }

        my $results = $yv_obj->next_page($code, %args);

        if (@{$results->{results}}) {
            $liststore->set(
                $iter, 0,
                get_next_page_spaces() . do {
                    $code =~ /[&?]start-index=(\d+)/;
                    '<big><b>Page: ' . ((($1 - 1) / $CONFIG{results}) + 2) . '</b></big>';
                  }
            );
            $liststore->set($iter, 3, q{});
        }
        else {
            $liststore->remove($iter);
            die "This is the last page!\n";
        }

            $args{playlists}           ? print_playlists($results)
          : $args{channels}            ? print_channels($results)
          : $args{channel_suggestions} ? print_channels($results)
          :                              print_videos($results);
      }
      : $code =~ /^username=(.+)/     ? videos_from_username($1)
      : $code =~ /$valid_video_id_re/ ? play_videos([{videoID => $code}])
      :                                 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, $search_type) = @_;
    my $iter = $liststore->append;
    $liststore->set($iter, 0, get_next_page_spaces() . "<big><b>NEXT PAGE</b></big>");
    $liststore->set($iter, 3, $url);
    $liststore->set($iter, 5, $search_type);
}

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 ($thumbnail) {
        my $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 print_videos {
    my ($results) = @_;

    my $url    = $results->{url};
    my $videos = $results->{results};

    unless (@{$videos}) {
        die "No video results...\n";
    }

    hide_feeds_window();
    $search_for_combobox->set_active(0);

    foreach my $video (@{$videos}) {
        my $iter = $liststore->append;

        my $row_description = $video->{description} || 'No description available...';
        $liststore->set($iter, 4, $row_description);
        $liststore->set($iter, 6, $video->{author});
        $row_description = _make_row_description($row_description);

        $liststore->set($iter, 3, $video->{videoID});
        $liststore->set(
                        $iter,
                        0,
                        "<big><b>"
                          . encode_entities($video->{title} // 'Unknown')
                          . "</b></big>\n\n"
                          . "<b>Likes\t:</b> "
                          . $yv_utils->set_thousands($video->{likes}) . "\n"
                          . "<b>Dislikes\t:</b> "
                          . $yv_utils->set_thousands($video->{dislikes}) . "\n"
                          . "<b>Favorited\t:</b> "
                          . $yv_utils->set_thousands($video->{favorited}) . "\n"
                          . "<b>Category\t:</b> "
                          . encode_entities($video->{category} // 'Unknown') . "\n"
                          . "<b>Publisher\t: </b>"
                          . $video->{author} . "\n\n" . "<i>"
                          . encode_entities($row_description) . "</i>"
                       );

        $liststore->set(
            $iter, 2,
            "<b>Length\t:</b> "
              . $yv_utils->format_time($video->{duration}) . "\n"
              . "<b>Rating\t:</b> "
              . sprintf('%.2f', ($video->{rating} || 0)) . "\n"
              . "<b>Views\t:</b> "
              . $yv_utils->set_thousands($video->{views})
              . do {
                defined $video->{published}
                  ? "\n" . "<b>Added\t: </b>" . $yv_utils->format_date($video->{published})
                  : q{};
              }
        );

        if ($CONFIG{show_thumbs}) {
            my $thumb_url = sprintf($CONFIG{thumb_url}, $video->{videoID}, $CONFIG{default_thumb});
            my $pixbuf = _get_pixbuf_thumbnail($thumb_url);
            $liststore->set_value($iter, 1, $pixbuf);
        }
    }

    _append_next_page($url);
}

sub print_channel_suggestions {
    my $results = $yv_obj->get_channel_suggestions();
    $liststore->clear if $CONFIG{clear_list};
    print_channels($results, 'channel_suggestions');
}

sub print_channels {
    my ($results, $type) = @_;

    my $url      = $results->{url};
    my $channels = $results->{results};

    unless (@{$channels}) {
        die "No channel results...\n";
    }

    $search_for_combobox->set_active(1);

    foreach my $channel (@{$channels}) {
        my $iter = $liststore->append;
        my $row_description = $channel->{summary} || 'No description available...';

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

        $channel->{title} =~ s{<.*?>}{}sg;
        $channel->{title} = decode_entities($channel->{title})
          if $channel->{title} =~ /&#?\w/;

        $liststore->set($iter, 3, "username=$channel->{author}");
        $liststore->set(
                        $iter, 0,
                        '<big><b>'
                          . encode_entities($channel->{title})
                          . "</b></big>\n\n"
                          . "<b>Author\t:</b> "
                          . $channel->{name} . "\n"
                          . "<b>Subscr\t:</b> "
                          . $yv_utils->set_thousands($channel->{subscribers}) . "\n"
                          . (
                             exists($channel->{views})
                             ? ("<b>Views\t:</b> " . $yv_utils->set_thousands($channel->{views}))
                             : ("<b>Videos\t:</b> " . $yv_utils->set_thousands($channel->{videos}))
                            )
                          . "\n\n<i>"
                          . encode_entities($row_description) . '</i>'
                       );

        $liststore->set($iter, 2, "<b>Updated:</b> " . $yv_utils->format_date($channel->{updated}) . "\n\n");

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

    _append_next_page($url, $type // 'channels');
}

#---------------------- PRINT PLAYLISTS ----------------------#
sub print_playlists {
    my ($results) = @_;

    my $url       = $results->{url};
    my $playlists = $results->{results};

    unless (@{$playlists}) {
        die "No playlist results...\n";
    }

    hide_feeds_window();
    $search_for_combobox->set_active(2);

    foreach my $playlist (@{$playlists}) {

        next if $playlist->{count} == 0;

        my $iter = $liststore->append;
        my $row_description = $playlist->{summary} || 'No description available...';

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

        $liststore->set($iter, 6, $playlist->{author});
        $liststore->set($iter, 3, $playlist->{playlistID});
        $liststore->set(
                        $iter,
                        0,
                        '<big><b>'
                          . encode_entities($playlist->{'title'})
                          . "</b></big>\n\n"
                          . "<b>Author\t:</b> "
                          . $playlist->{name} . "\n"
                          . "<b>Published\t:</b> "
                          . $yv_utils->format_date($playlist->{published}) . "\n"
                          . "<b>Updated\t:</b> "
                          . $yv_utils->format_date($playlist->{updated}) . "\n\n" . '<i>'
                          . encode_entities($row_description) . '</i>'
                       );

        $liststore->set($iter, 2, "<b>Videos:</b> " . $yv_utils->set_thousands($playlist->{count}) . "\n\n");

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

    _append_next_page($url, 'playlists');
}

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

    my $info = $yv_obj->get_videos_from_playlist($playlistID);
    if (@{$info->{results}}) {
        $liststore->clear if $CONFIG{clear_list};
        print_videos($info);
        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_from_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);
    videos_from_username($username);
}

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

sub videos_from_text_entry_username {
    videos_from_username($_[0]->get_text);
}

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

    my $videos = $yv_obj->get_videos_from_username($username);
    if (@{$videos->{results}}) {
        $liststore->clear if $CONFIG{clear_list};
        print_videos($videos);
    }
    else {
        die "No video uploaded by: <$username>\n";
    }
    return 1;
}

sub favorited_videos_from_username {
    my $username = ref $_[0] ? $_[0]->get_text : $_[0];

    is_valid_username($username) or return;
    my $videos = $yv_obj->get_favorited_videos_from_username($username);
    if (@{$videos->{results}}) {
        $liststore->clear if $CONFIG{clear_list};
        print_videos($videos);
    }
    else {
        die "No video favorited by username: <$username>\n";
    }
    return 1;
}

sub subscription_videos_from_username {
    my $username = ref $_[0] ? $_[0]->get_text : $_[0];
    is_valid_username($username) or return;
    newsubscriptionvideos($username);
}

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

sub playlists_from_username {
    my $username = ref $_[0] ? $_[0]->get_text : $_[0];
    is_valid_username($username) or return;

    my $playlists = $yv_obj->get_playlists_from_username($username);
    if (@{$playlists->{results}}) {
        $liststore->clear if $CONFIG{clear_list};
        print_playlists($playlists);
    }
    else {
        die "No playlists found.\n";
    }
}

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

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

    my @urls = $yv_obj->get_streaming_urls($video_id)
      or die "Can't get the streaming URLs for videoID: $video_id.\n";

    my $srt_file;

    my $has_cc;
    foreach my $url (@urls) {
        if (exists $url->{has_cc} and $url->{has_cc} =~ /^(?:true|yes)$/i) {
            $has_cc = 1;
            last;
        }
    }

    # Download the closed-captions
    if ($has_cc and $CONFIG{get_captions} and defined $CONFIG{gcap}) {
        require WWW::YoutubeViewer::GetCaption;
        my $yv_cap = WWW::YoutubeViewer::GetCaption->new(
                                                         captions_dir => $CONFIG{captions_dir},
                                                         gcap         => $CONFIG{gcap},
                                                         languages    => $CONFIG{srt_languages},
                                                        );
        $srt_file = $yv_cap->get_caption($video_id);
    }

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

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

    if (not defined $streaming) {
        $streaming = $yv_itags->find_streaming_url(\@urls, $CONFIG{prefer_webm});
    }

    my $info = @urls && ref $urls[-1] eq 'HASH' && exists $urls[-1]{status} ? $urls[-1] : {};

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

sub update_mplayer_arguments {
    my ($resolution) = @_;

    if ($CONFIG{use_lower_cache}
        or not $resolution ~~ [qw(original 1080 720)]) {
        set_mplayer_arguments($CONFIG{lower_cache}, $CONFIG{lower_cache_min});
    }
    else {
        set_mplayer_arguments($CONFIG{cache}, $CONFIG{cache_min});
    }
}

sub get_quotewords {
    require Text::ParseWords;
    return Text::ParseWords::quotewords(@_);
}

#---------------------- PLAY AN YOUTUBE VIDEO ----------------------#
sub play_videos {
    my ($videos) = @_;

    foreach my $video (@{$videos}) {
        my $streaming = get_streaming_url($video->{videoID});

        if (defined $streaming->{info}{status} and not $streaming->{info}{status} =~ /^(?:ok|success)/i) {
            die "(x_x) Can't stream: " . sprintf($CONFIG{youtube_video_url}, $video->{videoID}) . "\n",
              "(x_x) Status: $streaming->{info}{status}", 'bold red' . "\n\n";
        }

        if (not defined $streaming->{streaming}) {
            next;
        }

        update_mplayer_arguments($streaming->{resolution});

        my @mplayer_line = (
                            $CONFIG{mplayer},
                            get_quotewords(
                                           qr/\s+/, 1,
                                           join(
                                                q{ },
                                                (
                                                 defined $streaming->{srt_file}
                                                 ? sprintf($CONFIG{mplayer_srt_args}, $streaming->{srt_file})
                                                 : ()
                                                ),
                                                grep({defined and /\S/} values %MPLAYER),
                                               )
                                          )
                           );

        if ($yv_obj->get_debug) {
            print STDERR "@mplayer_line\n";
            print STDERR "$streaming->{streaming}{url}\n" if $yv_obj->get_debug() == 2;
            @mplayer_line = grep { !/^--?really-quiet\z/ } @mplayer_line;
        }

        # Execute mplayer:
        system join(q{ }, map { quotemeta } @mplayer_line, $streaming->{streaming}{url}) . q{ &};
    }

    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->get_videos_from_category($cat_id);

    if (@{$videos->{results}}) {
        $liststore->clear if $CONFIG{clear_list};
        print_videos($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_list};
    print_videos(
                   $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_external_program($CONFIG{terminal}, '-e', q{'} . join(' ', $CONFIG{perl_bin}, $CONFIG{cli_youtube_viewer}),
                             q{'});
}

sub find_terminal {
    foreach my $term (
                      'gnome-terminal', 'lxterminal', 'terminal', 'xfce4-terminal',
                      'sakura',         'lilyterm',   'evilvte',  'superterm',
                      'terminator',     'kterm',      'mlterm',   'mrxvt',
                      'rxvt',           'urxvt',      'uuterm',   'termit',
                      'fbterm',         'stjerm',     'yakuake',  'roxterm'
      ) {
        foreach my $path (path()) {
            if (-e (my $terminal = catfile($path, $term))) {
                return $terminal;
            }
        }
    }
    return $ENV{TERM} || 'xterm';
}

sub get_options_as_arguments {
    my @args;
    my %options = (
                   'no-interactive' => q{},
                   'orderby'        => $CONFIG{orderby},
                   'time'           => $CONFIG{time_sort},
                   'resolution'     => $CONFIG{resolution},
                   'download-dir'   => rel2abs($CONFIG{downloads_folder}),
                   'fullscreen'     => $CONFIG{fullscreen} ? q{} : undef,
                  );

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

sub execute_external_program {
    system join(' ', map { chr ord eq q{'} ? $_ : quotemeta } @_) . ' &';
}

sub _make_youtube_url {

    my $format =
        $CONFIG{search_playlists} ? $CONFIG{playlists_url}
      : $CONFIG{search_channels}  ? $CONFIG{youtube_profile_url}
      :                             $CONFIG{youtube_video_url};

    my $value = $_[0] =~ /^username=(.+)/ ? $1 : shift;

    return sprintf($format, $value);
}

sub open_youtube_url {
    my $url = _make_youtube_url(get_selected_video_code() or return);
    execute_external_program((defined $CONFIG{browser} ? $CONFIG{browser} : 'xdg-open'), $url);
    return 1;
}

my @queue_codes;

sub queue_playback {
    my $code = get_selected_video_code() or 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_video_code() or return;
    execute_cli_youtube_viewer("--video-id=$code");
    return 1;
}

sub execute_cli_youtube_viewer {
    my @arguments = @_;

    # Executing youtube-viewer
    my $command = join(
                       q{ },
                       (
                        map { chr ord eq q{'} ? $_ : quotemeta } $CONFIG{terminal},
                        '-e',
                        q{'}
                          . join(q{ },
                                 $CONFIG{perl_bin},          $CONFIG{cli_youtube_viewer},
                                 get_options_as_arguments(), @arguments)
                          . q{'}
                       )
                      )
      . ' &';

    say $command if $yv_obj->get_debug;
    system $command;

    warn "youtube-viewer - code exit: $?\n" if $?;

    return 1;
}

sub download_video {
    my $code = get_selected_video_code() or return;
    die "Unable to download a playlist!\n" if $CONFIG{search_playlists};
    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 $comments = $yv_obj->next_page($value, comments => 1);
        if (@{$comments->{results}}) {
            print_comments($comments);
        }
        else {
            die "This is the last page of comments.\n";
        }
    }

    return 1;
}

sub get_user_favorited_videos {
    my $username = get_username_for_selected_video() // return;
    favorited_videos_from_username($username);
}

sub get_username_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_video_code() or return;

    my $videos = $yv_obj->get_related_videos($code);
    if (@{$videos->{results}}) {
        $liststore->clear if $CONFIG{clear_list};
        print_videos($videos);
    }
    else {
        die "No related video for videoID: <$code>\n";
    }
}

sub favorite_video {
    my $code = get_selected_video_code() or return;

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

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

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

sub dislike_selected_video {
    my $code = get_selected_video_code() or 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_video_code() or return;
    my $comment = get_text($gui->get_object('comment_textview'));

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

sub print_comments {
    my ($results, %options) = @_;

    my $url      = $results->{url};
    my $comments = $results->{results};

    if (not @{$comments}) {
        return;
    }

    my $i = 0;
    foreach my $comment (@{$comments}) {

        my $iter = $feeds_liststore->append;
        $feeds_liststore->set(
                              $iter,
                              0,
                              "<big><b>$comment->{name}</b> ("
                                . $yv_utils->format_date($comment->{published})
                                . ") said:</big>\n\t"
                                . encode_entities($comment->{content})
                             );
    }

    my $iter = $feeds_liststore->append;
    $feeds_liststore->set($iter, 0, "\n<big><b>=&gt;&gt; NEXT PAGE</b></big>\n");
    $feeds_liststore->set($iter, 1, $url);
    return 1;
}

sub show_more_videos_from_username {
    videos_from_username(get_username_for_selected_video() or return);
}

sub show_playlists_from_username {
    playlists_from_username(get_username_for_selected_video() or return);
}

# Setting details to details_window
sub set_video_details {
    my ($code, $iter) = @_;
    my $main_details = $liststore->get($iter, 0);

    my %labels = (
                  Added  => 'Published',
                  Length => 'Duration',
                 );

    # 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/\s+/$-[0] <= 8 ? "\t\t" : "\t"/e;

    my $secondary_details = $liststore->get($iter, 2);
    $secondary_details =~ s/:(?!\d)/\t: /g;
    $secondary_details =~ s/\b$_\b\t/$labels{$_}/ for keys %labels;
    $gui->get_object('video_details_label')->set_label($main_details . $secondary_details);

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

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

    # 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;
    exit 0;
}

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

'Gtk2'->main;
