#!/usr/local/bin/perl
use warnings;
use strict;
use 5.010000;
use utf8;

our $VERSION = '0.031';

use Encode                qw( decode );
use File::Spec::Functions qw( catdir catfile curdir );
use Getopt::Long          qw( GetOptions );
use Pod::Usage            qw( pod2usage );

use Encode::Locale         qw( decode_argv );
use File::HomeDir          qw();
use File::Which            qw( which );
use IPC::System::Simple    qw( capture );
use Term::ANSIScreen       qw( :cursor :screen );
use Term::Choose           qw( choose );
use Term::ReadLine::Simple qw();
use Try::Tiny              qw( try catch );

use if $^O eq 'MSWin32', 'Win32::Console::ANSI';
print "\e(U" if $^O eq 'MSWin32';

use App::YTDL::Config   qw( read_config_file set_options );
use App::YTDL::Videos   qw( from_arguments_to_choices );
use App::YTDL::Download qw( download_youtube );
use App::YTDL::Info     qw( get_download_infos );
use App::YTDL::Helper   qw( encode_fs );

binmode STDIN,  ':encoding(console_in)';
binmode STDOUT, ':encoding(console_out)';
binmode STDERR, ':encoding(console_out)';

my ( $arg_file, $help );
GetOptions( 'f|file=s@' => \$arg_file, 'h|?|help' => \$help )
or pod2usage( -message => $!, -verbose => 99, -sections => "SYNOPSIS" );


my $my_data    = decode( 'locale_fs', File::HomeDir->my_data   || curdir );
my $my_videos  = decode( 'locale_fs', File::HomeDir->my_videos || curdir );
my $config_dir = catdir $my_data, 'yt_download';

if ( ! -d encode_fs( $config_dir ) ) {
    mkdir encode_fs( $config_dir ) or die $!;
}

my $opt = {
    useragent      => 'Mozilla/5.0',
    video_dir      => $my_videos,
    config_dir     => $config_dir,
    config_file    => catfile( $config_dir, 'yt_config.txt' ),
    log_file       => catfile( $config_dir, 'yt_download.log' ),
    log_info       => 0,
    c_history_file   => catfile( $config_dir, 'yt_channel_history.txt' ),
    c_history_sticky => catfile( $config_dir, 'yt_channel_sticky.txt' ),
    max_len_f_name => 62,
    linefold       => { Charset => 'utf-8', Newline => "\n", OutputCharset => '_UNICODE_', Urgent => 'FORCE' },
    yt_api_v       => 2,
    invalid_char   => quotemeta( '#$&+,/:;=?@' ),
    yt_regexp      => qr/(?:youtube|youtu\.be|yourepeat|tube\.majestyc)/i,
    kb_sec_len     => 5,
    max_info_width => 120,
    right_margin   => $^O eq 'MSWin32' ? 1 : 2,
    retries        => 5,
    timeout        => 15,
    overwrite      => 0,
    auto_quality   => 1,
    preferred      => [ 43 ],
    max_channels   => 15,
    extractor_dir  => 1,
    channel_dir    => 1,
    new_first      => 1,
    error_get_download_infos => [],
    download_status_not_ok   => [],
    incomplete_download      => [],
};

read_config_file( $opt, $opt->{config_file} );
if ( $help ) {
    set_options( $opt );
}

try {
    my $youtube_dl = which( 'youtube-dl' ) // 'youtube-dl';
    my $capture = capture( $youtube_dl, '--version' );
}
catch {
    say "Could not find 'youtube-dl' - 'youtube-dl' is required - http://rg3.github.io/youtube-dl/.";
    exit 1;
};

local $| = 1;
print locate( 1, 1 ), cldown;



_read_history_files( $opt );

my @ids = _gather_arguments( $opt, $arg_file, @ARGV );

my $info = from_arguments_to_choices( $opt, @ids );

get_download_infos( $opt, $info );

download_youtube( $opt, $info );



my $error;
if ( @{$opt->{error_get_download_infos}} ) {
    print "\n";
    say "Error fetching download infos:";
    for my $video_id ( @{$opt->{error_get_download_infos}} ) {
        say '  ' . $video_id;
    }
    $error++;
}
if ( @{$opt->{download_status_not_ok}} ) {
    print "\n";
    say "Download status not ok:";
    for my $video_id ( @{$opt->{download_status_not_ok}} ) {
        say '  ' . $video_id;
    }
    $error++;
}
if ( @{$opt->{incomplete_download}} ) {
    print "\n";
    say "Incomplete_download:";
    for my $video_id ( @{$opt->{incomplete_download}} ) {
        say '  ' . $video_id;
    }
    $error++;
}
say "" if $error;



sub _read_history_files {
    my ( $opt ) = @_;
    $opt->{channel_sticky}  = [];
    $opt->{channel_history} = [];
    if ( $opt->{max_channels} ) {
        if ( -e $opt->{c_history_sticky} ) {
            open my $fh, '<:encoding(UTF-8)', encode_fs( $opt->{c_history_sticky} ) or die $!;
            while ( my $line = <$fh> ) {
                chomp $line;
                next if $line =~ /^\s+\z/;
                push @{$opt->{channel_sticky}}, $line;
            }
            close $fh;
        }
        if ( -e $opt->{c_history_file} ) {
            open my $fh, '<:encoding(UTF-8)', encode_fs( $opt->{c_history_file} ) or die $!;
            while ( my $line = <$fh> ) {
                chomp $line;
                next if $line =~ /^\s+\z/;
                push @{$opt->{channel_history}}, $line;
            }
            close $fh;
        }
    }
}


sub _gather_arguments {
    my ( $opt, $arg_file, @ids ) = @_;
    for my $file ( @$arg_file ) {
        open my $fh, '<:encoding(utf-8)', encode_fs( $file ) or die $!;
        while ( my $line = <$fh> ) {
            next if $line =~ /^\s*\z/;
            next if $line =~ /^\s*#/;
            $line =~ s/^\s+|\s+\z//g;
            push @ids, split /\s+/, $line;
        }
        close $fh or die $!;
    }
    if ( ! @ids ) {
        my $trs = Term::ReadLine::Simple->new();
        my $ids = $trs->readline( 'Enter url/id: ' );
        @ids = split /\s+/, $ids;
        print up( 1 ), cldown;
    }
    if ( ! @ids && ( @{$opt->{channel_history}} || @{$opt->{channel_sticky}} ) ) {
        MENU : while ( 1 ) {
            my ( $channel, $sticky ) = ( '  Channel', '  Sticky' );
            # Choose
            my $choice = choose(
                [ undef, $channel, $sticky ],
                { prompt => 'Choose:', layout => 3, undef => '  QUIT' }
            );
            if ( ! defined $choice ) {
                exit;
            }
            elsif ( $choice eq $channel ) {
                my @sticky  = map { ( split /,/, $_ )[0] } @{$opt->{channel_sticky}};
                my @history = map { ( split /,/, $_ )[0] } @{$opt->{channel_history}};
                my $prompt = 'Channels:' . "\n";
                CHANNEL: while ( 1 ) {
                    my @pre = ( undef, '  CONFIRM' );
                    my $choices = [ @pre, map( "* $_", @sticky ), map( "  $_", @history ) ];
                    # Choose
                    my @indexes = choose(
                        $choices,
                        { prompt => $prompt . "\nYour choice:", layout => 3, index => 1,
                          undef => '  BACK', no_spacebar => [ 0 .. $#pre, $#$choices ] }
                    );
                    if ( ! $indexes[0] ) {
                        @ids = ();
                        next MENU;
                    }
                    elsif ( $indexes[0] == 1 ) {
                        shift @indexes;
                        for my $i ( @indexes ) {
                            $i -= @pre;
                            if ( $i <= $#sticky ) {
                                push @ids, 'c#' . ( split /,/, $opt->{channel_sticky}[$i] )[1];
                                $prompt .= sprintf "- %s (%s)\n", split /,/, $opt->{channel_sticky}[$i];
                            }
                            else {
                                $i -= @{$opt->{channel_sticky}};
                                push @ids, 'c#' . ( split /,/, $opt->{channel_history}[$i] )[1];
                                $prompt .= sprintf "- %s (%s)\n", split /,/, $opt->{channel_history}[$i];
                            }
                        }
                        last MENU;
                    }
                    else {
                        for my $i ( @indexes ) {
                            $i -= @pre;
                            if ( $i <= $#sticky ) {
                                push @ids, 'c#' . ( split /,/, $opt->{channel_sticky}[$i] )[1];
                                $prompt .= sprintf "- %s (%s)\n", split /,/, $opt->{channel_sticky}[$i];
                            }
                            else {
                                $i -= @{$opt->{channel_sticky}};
                                push @ids, 'c#' . ( split /,/, $opt->{channel_history}[$i] )[1];
                                $prompt .= sprintf "- %s (%s)\n", split /,/, $opt->{channel_history}[$i];
                            }
                        }
                    }
                }
            }
            elsif ( $choice eq $sticky ) {
                my @sticky  = map { ( split /,/, $_ )[0] } @{$opt->{channel_sticky}};
                my @history = map { ( split /,/, $_ )[0] } @{$opt->{channel_history}};
                my @backup_channel_sticky  = @{$opt->{channel_sticky}};
                my @backup_channel_history = @{$opt->{channel_history}};
                my $changed = 0;
                STICKY: while ( 1 ) {
                    my @pre = ( undef, '  CONFIRM' );
                    my $idx = choose(
                        [ @pre, map( "+ $_", @sticky ), map( "- $_", @history ) ],
                        { prompt => 'Choose:', layout => 3, index => 1, undef => '  BACK' }
                    );
                    if ( ! $idx ) {
                        @{$opt->{channel_sticky}}  = @backup_channel_sticky;
                        @{$opt->{channel_history}} = @backup_channel_history;
                        @sticky  = map { ( split /,/, $_ )[0] } @{$opt->{channel_sticky}};
                        @history = map { ( split /,/, $_ )[0] } @{$opt->{channel_history}};
                        next MENU;
                    }
                    elsif ( $idx == 1 ) {
                        if ( $changed ) {
                            open my $fh_sticky, '>:encoding(UTF-8)', encode_fs( $opt->{c_history_sticky} ) or die $!;
                            for my $line ( @{$opt->{channel_sticky}} ) {
                                say $fh_sticky $line;
                            }
                            close $fh_sticky;
                            open my $fh_history, '>:encoding(UTF-8)', encode_fs( $opt->{c_history_file} ) or die $!;
                            for my $line ( @{$opt->{channel_history}} ) {
                                say $fh_history $line;
                            }
                            close $fh_history;
                        }
                        next MENU;
                    }
                    else {
                        $changed++;
                        $idx-= @pre;
                        if ( $idx > $#{$opt->{channel_sticky}} ) {
                            $idx -= @{$opt->{channel_sticky}};
                            push @{$opt->{channel_sticky}}, splice @{$opt->{channel_history}}, $idx, 1;
                            @{$opt->{channel_sticky}} = sort @{$opt->{channel_sticky}};
                        }
                        else {
                            push @{$opt->{channel_history}}, splice @{$opt->{channel_sticky}}, $idx, 1;
                        }
                        @sticky  = map { ( split /,/, $_ )[0] } @{$opt->{channel_sticky}};
                        @history = map { ( split /,/, $_ )[0] } @{$opt->{channel_history}};
                    }
                }
            }
        }
    }
    say "No arguments" and exit if ! @ids;
    return @ids;
}








__END__

=pod

=encoding UTF-8

=head1 NAME

yt-download - Download YouTube videos.

=head1 VERSION

Version 0.031

=cut

=head1 SYNOPSIS

    yt-download -h|-?|--help

    yt-download

    yt-download url [url ...]

    yt-download -f|--file filename

The urls can be entered after calling C<yt-download> - this is useful if urls contain shell metacharacters like C<&>.

The urls can also be passed with a file: C<yt-download -f|--file filename>. The urls in the file have to be space
separated.

If the extractor is YouTube, it is possible to pass ids instead of the entire urls. When passing only an id it is
required to prefix every playlist id with C<p#> and every channel id with C<c#>. Video ids are passed without any prefix.

=head1 DESCRIPTION

Download single videos or/and choose videos from playlists or/and channels.

When choosing from a channel or list it is possible to filter the displayed items with a regexp.

Before the download the script shows some video info and lets you choose the video quality from the available qualities.

Instead of choosing the quality manually it is possible to set and use preferred qualities. I<Preferred qualities> are
only valid for Youtube videos.

To set the different options call C<yt-download -h>.

C<App::YTDL> uses L<youtube-dl|http://rg3.github.io/youtube-dl/> to get the data required for the video download.

=head3 Legacy encodings

Non mappable characters on the output are replaced with C<*>. In file names they are replaced with C<&#xNNN;> where NNN
is the Unicode code point in a decimal number.

=head1 Options

=head2 HELP

Shows this HELP text.

=head2 PATH

Shows the version and the path of the running C<yt-download> and the path of the video directory and of the
configuration directory.

=head2 Download

=head3 UserAgent

Set the useragent.

If entered nothing the default useragent (Mozilla/5.0) is used.

=head3 Overwrite

If I<Overwrite> is enabled, existing files are overwritten.

If not enabled C<yt-download> appends to partially downloaded file with the same name.

=head3 Max filename length

Set the maximum length of the filename. Filenames longer as the maximum length are truncated.

=head3 Download retries

Set the number of download retries.

=head3 Timeout

I<Timeout> (seconds) is used as the value for the C<youtube-dl> parameter C<--socket-timeout>. I<timeout> is also used
as the value for the L<LWP::UserAgent> option C<timeout> when fetching the data required for the video download.

=head2 Quality

=head3 Auto quality mode

Set the auto quality (fmt) mode:

=over

=item

mode 0: choose always manually

=item

mode 1: keep the first quality chosen for a playlist/channel for all videos of that playlist/channel if possible.

=item

mode 2: keep the first chosen quality for all downloads if possible.

=item

mode 3: use preferred qualities (YouTube only).

=item

mode 4: use always the default (best) quality.

=back

=head3 Preferred qualities

Set the preferred qualities (fmts) for YouTube videos.

=head2 History

=head3 Logging

Enable info logging.

=head3 Channel history

Channel history works only for YouTube videos.

If I<Channel history> is set to "0" the channel history is disabled else the set value tells how many channels should be
saved in the channel history file (channels made sticky don't count).

If no arguments are passed to C<yt-download> the user can choose from the channels saved in the channel-history file and
the channel-sticky file.

=head2 Directory

=head3 Video directory

Choose an alternative video directory.

=head2 Extractor directory

=over

=item

0 => No.

=item

1 => Create/use extractor directories.

=back

=head3 Channel directory

=over

=item

0 => Don't create/use channel directories.

=item

1 => Create/use channel directories if the video is chosen from a channel or a list.

=item

2 => Always create/use channel directories.

=back

=head2 Output

=head3 Max info width

Set the maximum width of video info output.

=head3 Digits for "k/s"

Set the number of digits allocated for the "kilobyte per seconds" template.

=head3 Sort order

If set to "YES" the latest videos from a channel/list are on top of the list menu else they are at the end of the menu.

=head1 REQUIREMENTS

=head2 Perl version

Requires Perl version 5.10.0 or greater.

=head2 youtube-dl

L<youtube-dl|http://rg3.github.io/youtube-dl/> is required.

=head2 Encoding layer

For a correct output it is required an appropriate encoding layer for STDOUT matching the terminal's character set.

=head2 Monospaced font

It is required a terminal that uses a monospaced font which supports the printed characters.

=head1 CREDITS

C<App::YTDL> uses L<youtube-dl|http://rg3.github.io/youtube-dl/> to get the data required for the video download.

Thanks to the L<Perl-Community.de|http://www.perl-community.de> and the people form
L<stackoverflow|http://stackoverflow.com> for the help.

=head1 AUTHOR

Kuerbis <cuer2s@gmail.com>

=head1 LICENSE AND COPYRIGHT

Copyright (C) 2013-2015 Kuerbis.

This program is free software; you can redistribute it and/or modify it under the same terms as Perl 5.10.0. For
details, see the full text of the licenses in the file LICENSE.

=cut
