Group
Extension

WWW-Xunlei/lib/WWW/Xunlei.pm

package WWW::Xunlei;

use strict;
use warnings;

use LWP::UserAgent;
use HTTP::Request;
use URI::Escape;
use JSON;

use File::Basename;
use File::Path qw/mkpath/;
use Time::HiRes qw/gettimeofday/;
use POSIX qw/strftime/;
use Digest::MD5 qw(md5_hex);
use Term::ANSIColor qw/:constants/;
use Data::Dumper;

use WWW::Xunlei::Downloader;

our $DEBUG;

use constant URL_LOGIN  => 'http://login.xunlei.com/';
use constant URL_REMOTE => 'http://homecloud.yuancheng.xunlei.com/';
use constant DEFAULT_USER_AGENT =>
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:41.0) "
    . "Gecko/20100101 Firefox/41.0";
use constant URL_LOGIN_REFER => 'http://i.xunlei.com/login/2.5/?r_d=1';
use constant BUSINESS_TYPE   => '113';
use constant V               => '2';
use constant CT              => '0';

sub new {
    my $class = shift;
    my ( $user, $pass, %options ) = @_;
    my $self = {
        'ua'   => undef,
        'json' => undef,
        'user' => $user,
        'pass' => md5pass($pass),
    };

    my $cookie_file = $options{'cookie_file'};
    $self->{'ua'} = LWP::UserAgent->new;
    $self->{'ua'}->cookie_jar( { file => $cookie_file } );
    $self->{'ua'}->agent(DEFAULT_USER_AGENT);

    $self->{'json'} = JSON->new->allow_nonref;

    bless $self, $class;
    return $self;
}

sub get_downloaders {
    my $self = shift;

    my $parameters = { 'type' => 0, };

    my $res = $self->_yc_request( 'listPeer', $parameters );

    if ( $res->{'rtn'} != 0 ) {
        die "Unable to get the Downloader List: $@";
    }

    my @downloaders;
    for my $p ( @{ $res->{'peerList'} } ) {
        push @downloaders, WWW::Xunlei::Downloader->new( $self, $p );
    }

    return wantarray ? @downloaders : \@downloaders;
}

sub get_downloader {
    my $self = shift;
    
    my $name = shift;

    my @downloaders = grep { $_->{'name'} eq $name } $self->get_downloaders();
    die "No such Downloader named >>$name<<" unless @downloaders;
    return shift @downloaders;
}

sub bind {
    my $self = shift;

    my ( $key, $name ) = @_;

    my $parameters = {
        'boxname' => $name,
        'key'     => $key,
    };

    my $res = $self->_yc_request( 'bind', $parameters );
}

sub unbind {
    my $self = shift;
    my $pid  = shift;

    my $res = $self->_yc_request( 'unbind', { 'pid' => $pid } );
}

sub _login {
    my $self = shift;
    my $res = 1;
    unless ( $self->_is_session_expired ) {
        $res = $self->_session_login;
    }
    # sometimes the cookie( session_id ) is forced revoked from the 
    # server side. So we have to login with user/pass even it's not expired.
    $res = $self->_form_login if ( $res != 0 );

    die "Login Error: $res" if ( $res != 0 );
    $self->_set_auto_login();
    $self->_save_cookie();
}

sub _form_login {
    my $self        = shift;
    my $verify_code = uc $self->_get_verify_code();
    $self->_debug( "Verify Code: " . $verify_code );
    my $password   = md5_hex( $self->{'pass'} . $verify_code );
    my $parameters = {
        'u'          => $self->{'user'},
        'p'          => $password,
        'verifycode' => $verify_code,
    };

    # $self->{'ua'}->post(join( '/', URL_LOGIN, 'sec2login/'), $parameters);
    $self->_request( 'POST', URL_LOGIN . 'sec2login/', $parameters );

    return $self->_get_cookie('blogresult');
}

sub _session_login {
    my $self = shift;
    my $parameters = { 'sessionid' => $self->_get_cookie('_x_a_') };
    $self->_request( 'GET', URL_LOGIN . 'sessionid/', $parameters );
    return $self->_get_cookie('blogresult');
}

sub _is_logged_in {
    my $self = shift;
    return (   $self->_get_cookie('sessionid')
            && $self->_get_cookie('userid') );
}

sub _is_session_expired {
    my $self = shift;

    my $session_expired_time
        = $self->{'ua'}
        ->{'cookie_jar'}{'COOKIES'}{'.xunlei.com'}{'/'}{'_x_a_'}[5];
    return 1 unless $session_expired_time;
    return (gettimeofday)[0] > $session_expired_time;
}

sub _get_verify_code {
    my $self       = shift;
    my $parameters = {
        'u'             => $self->{'user'},
        'business_type' => BUSINESS_TYPE,
        'cachetime'     => int( gettimeofday() * 1000 ),
    };
    $self->_request( 'GET', URL_LOGIN . 'check/', $parameters );
    my $check_result = $self->_get_cookie('check_result');
    my $verify_code = ( split( ':', $check_result ) )[1];
    return $verify_code;
}

sub _set_auto_login {
    my $self      = shift;
    my $sessionid = $self->_get_cookie('sessionid');
    $self->_set_cookie( '_x_a_', $sessionid, 604800 );
}

sub _delete_temp_cookies {
    my $self = shift;
    my @login_cookie
        = qw/VERIFY_KEY verify_type check_n check_e logindetail result/;
    for my $c (@login_cookie) {
        $self->_delete_cookie($c);
    }
}

sub _get_cookie {
    my $self = shift;
    my ( $key, $domain, $path ) = @_;
    $domain ||= ".xunlei.com";
    $path   ||= "/";
    $self->{'ua'}->{'cookie_jar'}->{'COOKIES'}{$domain}{'/'}{$key}[1];
}

sub _set_cookie {
    my $self = shift;
    my ( $key, $value, $expire, $domain, $path ) = @_;
    $domain ||= ".xunlei.com";
    $path   ||= "/";
    $self->{'ua'}->{'cookie_jar'}
        ->set_cookie( undef, $key, $value, $path, $domain, undef,
        undef, undef, $expire );
    $self->{'ua'}->{'cookie_jar'}->{'COOKIES'}{$domain}{$path}{$key};
}

sub _save_cookie {
    my $self = shift;

    $self->_delete_cookie('blogresult');
    my $cookie_file = $self->{'ua'}->{'cookie_jar'}->{'file'};
    return unless $cookie_file;
    my $cookie_path = dirname($cookie_file);
    if ( !-d $cookie_path ) {
        mkpath($cookie_path);
    }
    $self->{'ua'}->{'cookie_jar'}->save();
}

sub _delete_cookie {
    my $self = shift;
    my ( $key, $domain, $path ) = @_;
    $domain ||= ".xunlei.com";
    $path   ||= "/";
    $self->{'ua'}->{'cookie_jar'}->clear($domain, $path, $key);
}

sub _yc_request {
    my $self = shift;
    my ( $action, $parameters, $data ) = @_;

    my $method = $data ? 'POST' : 'GET';
    my $uri = URL_REMOTE . $action;
    $parameters->{'v'}  = V;
    $parameters->{'ct'} = CT;

    $self->_login unless $self->_is_logged_in;
    my $res = $self->_request( $method, $uri, $parameters, $data );
    if ( $res->{'rtn'} != 0 ) {

        # Todo: Handling not login failed here.
        die "Request Error: $res->{'rtn'}";
    }

    return $res;
}

sub _request {
    my $self = shift;
    my ( $method, $uri, $parameters, $postdata ) = @_;
    my ( $form_string, $payload );
    if ($parameters) {
        $form_string = urlencode($parameters);
    }

    if ( $method ne 'GET' && !$postdata ) {

        # use urlencode parameters as post data when posting login form.
        $payload = $form_string;
    }
    else {
        $uri .= '?' . $form_string;
        if ( ref $postdata ) {
            $payload = $self->{'json'}->encode($postdata);
            $payload = urlencode( { 'json' => $payload } );
        }
    }

    my $request = HTTP::Request->new( $method => $uri, undef, $payload );
    $request->header( 'Content-Type' => 'application/x-www-form-urlencoded' );
    $self->_debug($request);
    my $response = $self->{'ua'}->request($request);
    die $response->code . ":" . $response->message
        unless $response->is_success;
    my $content = $response->content;

    $self->_debug($content);

    $content =~ s/\s$//g;
    return "" unless ( length($content) );

    return $self->{'json'}->decode($content) if ( $content =~ /\s*[\[\{\"]/ );
}

sub urlencode {
    my $data = shift;

    my @parameters;
    for my $key ( keys %$data ) {
        push @parameters,
            join( '=', map { uri_escape_utf8($_) } $key, $data->{$key} );
    }
    my $encoded_data = join( '&', @parameters );
    return $encoded_data;
}

sub md5pass {
    my $pass = shift;
    if ( $pass !~ /^[0-9a-f]{32}$/i ) {
        $pass = md5_hex( md5_hex($pass) );
    }
    return $pass;
}

sub _debug {
    my $self    = shift;
    my $message = shift;
    if ($DEBUG) {
        if ( ref $message ) { $message = Dumper($message); }
        my $date = strftime( "%Y-%m-%d %H:%M:%S", localtime );

        #$date .= sprintf(".%03f", current_timestamp());
        print BLUE "[ $date ] ", GREEN $message, RESET "\n";
    }
}

1;

=pod

=encoding UTF-8

=head1 NAME

WWW::Xunlei - Perl API For Official Xunlei Remote API.

=head1 VERSION

version 0.2

=head1 SYNOPSIS

    use WWW::Xunlei;
    my $client = WWW::Xunlei->new("username", "password");
    # use the first downloader;
    my $downloader = $client->get_downloaders()->[0];
    # create a remote task;
    $downloader->create_task("http://www.cpan.org/src/5.0/perl-5.22.0.tar.gz");

=head1 DESCRIPTION

C<WWW::Xunlei> is a Perl Wrapper of Xunlei Remote Downloader API.
L<Official Site of Xunlei Remote|http://yuancheng.xunlei.com>

=head1 METHODS

=head2 new( $username, $password, [cookie_file=>'/path/to/cookie'])

create a Xunlei client. Load or save Cookies to a plain text file with 
C<cookie_file> option. The default session expire time is 7 days.

=head2 bind($key, [$name])

Bind a new downloader with a activation code. The new downloader's name can
 be defined with the optional argument C<$name>.

=head2 get_downloaders

List all the downloaders binding with your account. Return a list of
C<WWW::Xunlei::Downloader> object.

=head2 get_downloader($name)

Get the downloader of which the name is $name. 
Return a C<WWW::Xunlei::Downloader> object.

=head1 AUTHOR

Zhu Sheng Li <zshengli@cpan.org>

=head1 COPYRIGHT AND LICENSE

This software is Copyright (c) 2015 by Zhu Sheng Li.

This is free software, licensed under:

  The MIT (X11) License

=cut

__END__

# ABSTRACT: Perl API For Official Xunlei Remote API.



Powered by Groonga
Maintained by Kenichi Ishigaki <ishigaki@cpan.org>. If you find anything, submit it on GitHub.