371 lines
9.7 KiB
Perl
371 lines
9.7 KiB
Perl
#!/usr/bin/env perl
|
|
package pcurse;
|
|
use strict;
|
|
use DateTime;
|
|
use IO::Socket::SSL;
|
|
use Getopt::Long;
|
|
use Archive::Extract;
|
|
use Thread::Pool;
|
|
use JSON;
|
|
use LWP::UserAgent;
|
|
use HTML::HTML5::Parser;
|
|
use feature ':5.10';
|
|
|
|
sub merge_opts {
|
|
my $opts = shift;
|
|
my $conf = shift;
|
|
if(defined($opts->{'debug'})) {
|
|
print 'We got opts:'."\n";
|
|
print Dumper $opts;
|
|
print 'We got conf:'."\n";
|
|
print Dumper $conf;
|
|
}
|
|
foreach my $k(keys %{$opts}) {
|
|
next unless(defined($opts->{$k}));
|
|
$conf->{$k} = $opts->{$k};
|
|
}
|
|
return $conf;
|
|
}
|
|
|
|
sub parse_arguments {
|
|
my $toret;
|
|
Getopt::Long::GetOptions (
|
|
"verbose" => \$toret->{'verbose'},
|
|
"wowpath=s" => \$toret->{'wowpath'},
|
|
"baseuri=s" => \$toret->{'baseuri'},
|
|
"config=s" => \$toret->{'config'},
|
|
"test" => \$toret->{'test'},
|
|
"workers=i" => \$toret->{'workers'},
|
|
"debug" => \$toret->{'debug'},
|
|
"add=s" => \$toret->{'add'},
|
|
"name=s" => \$toret->{'name'},
|
|
);
|
|
return $toret;
|
|
}
|
|
|
|
sub load_config {
|
|
my $file = shift;
|
|
my $toret;
|
|
unless(-e $file) {
|
|
my @p = split(/\//, $file);
|
|
my $file = pop(@p);
|
|
my $path = join('/', @p);
|
|
unless(-d $path) {
|
|
print 'Will create path: '.$path."\n";
|
|
system("mkdir","-p","$path");
|
|
}
|
|
} else {
|
|
$toret = pcurse::import_json($file);
|
|
}
|
|
$toret = pcurse::sane_defaults($toret);
|
|
return $toret;
|
|
}
|
|
|
|
sub check_config {
|
|
my $conf = shift;
|
|
unless($conf->{'wowpath'}) {
|
|
print 'Where is your addons installed? (complete path, including AddOns on the end): ';
|
|
while(my $line = <>) {
|
|
chomp($line);
|
|
if(-e $line) {
|
|
$conf->{'wowpath'} = $line;
|
|
last;
|
|
}
|
|
print 'You sure? Cannot read that path. Try again: ';
|
|
}
|
|
}
|
|
return $conf;
|
|
}
|
|
|
|
sub sane_defaults {
|
|
my $in = shift;
|
|
$in->{'addons'} = $ENV{'HOME'}.'/.pcurse/addons.json' unless(exists($in->{'addons'}));
|
|
$in->{'workers'} = "4" unless(exists($in->{'workers'}));
|
|
return $in;
|
|
}
|
|
|
|
sub load_addons {
|
|
my $addons_file = shift;
|
|
unless(-e $addons_file) {
|
|
my @parts = split(/\//, $addons_file);
|
|
my $f = pop(@parts);
|
|
my $d = join('/', @parts);
|
|
unless(-e $d) {
|
|
system("mkdir","-p",$d);
|
|
print 'Created '.$d."\n";
|
|
}
|
|
if(-e $ENV{'HOME'}.'/.lcurse/addons.json') {
|
|
print 'There seems to be an addons.json from lcurse around, and we have no list ourself yet. Stealing it:)'."\n";
|
|
my $json = &import_json($ENV{'HOME'}.'/.lcurse/addons.json');
|
|
$json = $json->{'addons'};
|
|
$json = pcurse::add_baseuri_to_addon($json);
|
|
return $json;
|
|
}
|
|
} else {
|
|
my $json = &import_json($addons_file);
|
|
$json = pcurse::add_baseuri_to_addon($json);
|
|
return $json;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
sub add_baseuri_to_addon {
|
|
my $json = shift;
|
|
foreach my $addon(@{$json}) {
|
|
unless(exists($addon->{'host'})) {
|
|
my $str = $addon->{'uri'};
|
|
my (undef,$h,undef,$u) = split(/(http(|s)\:\/\/[a-zA-Z0-9.]+)(\/.+)/,$str,2);
|
|
$addon->{'uri'} = $u;
|
|
$addon->{'host'} = $h;
|
|
}
|
|
#$addon->{'uri'} = 'https://www.curseforge.com'.$addon->{'uri'} unless($addon->{'uri'} =~ m/^http/);
|
|
#$addon->{'baseuri'} =
|
|
}
|
|
return $json;
|
|
}
|
|
|
|
sub save_config {
|
|
my $json = JSON->new;
|
|
$json->convert_blessed;
|
|
$json->allow_nonref;
|
|
$json->allow_tags;
|
|
$json->allow_blessed;
|
|
my $file = shift;
|
|
my $json_data = shift;
|
|
if(ref $json_data eq 'ARRAY' or ref $json_data eq 'HASH') {
|
|
my $text = $json->pretty->encode($json_data);
|
|
open my $fh, ">", $file or return (0,'Could not open '.$file.' for writing: '.$!);
|
|
print $fh $text;
|
|
close $fh;
|
|
return (1,$file.' saved successfully');
|
|
} else {
|
|
say 'Invalid format on passed data (not a HASH or ARRAY ref)';
|
|
}
|
|
}
|
|
|
|
sub import_json {
|
|
my $file = shift;
|
|
my $json = JSON->new;
|
|
my $json_data = do {
|
|
local $/ = undef;
|
|
open my $fh, "<", $file or die 'Could not read file '.$file.': '.$!;
|
|
<$fh>;
|
|
};
|
|
my $toret = $json->decode($json_data);
|
|
return $toret;
|
|
}
|
|
|
|
sub html_parse {
|
|
my $parser = HTML::HTML5::Parser->new();
|
|
my $html = shift;
|
|
my %parseopts = ();
|
|
my $doc = $parser->parse_string($html,\%parseopts);
|
|
my $doctype = $parser->dtd_element($doc);
|
|
return $html unless(defined($doctype));
|
|
return $doc;
|
|
}
|
|
|
|
sub http_get {
|
|
my $uri = shift;
|
|
my $ua = LWP::UserAgent->new(timeout => 10);
|
|
$ua->agent('Mozilla/5.0 (Linux; Android 5.1.1; Nexus 5 Build/LMY48B; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/43.0.2357.65 Mobile Safari/537.36');
|
|
my $response = $ua->get($uri);
|
|
return $response if($response->is_success);
|
|
return undef;
|
|
}
|
|
|
|
sub html_get {
|
|
my $uri = shift;
|
|
my $response = pcurse::http_get($uri);
|
|
return undef unless($response);
|
|
my $html = pcurse::html_parse($response->decoded_content);
|
|
return $html;
|
|
}
|
|
|
|
sub get_latest_file_id {
|
|
my $html = shift;
|
|
my $uri = shift;
|
|
return 'elvui' if($uri =~ m/tukui/);
|
|
$uri .= '/download/';
|
|
my $retstr = pcurse::find_in_html('dlstring',$html,$uri);
|
|
return $retstr;
|
|
}
|
|
|
|
sub get_product_version {
|
|
my $html = shift;
|
|
my $uri = shift;
|
|
my $fileid = shift;
|
|
if($fileid eq 'elvui') {
|
|
$uri = '/downloads/';
|
|
} else {
|
|
$uri .= '/files/'.$fileid;
|
|
}
|
|
my $retstr = pcurse::find_in_html('vstring',$html,$uri);
|
|
return $retstr;
|
|
}
|
|
|
|
sub find_in_html {
|
|
my $mode = shift;
|
|
my $html = shift;
|
|
my $sstring = shift;
|
|
unless($sstring =~ m/\/downloads\//) { #ElvUI hacks
|
|
my @sstringa = split /\//, $sstring,4;
|
|
$sstring = pop(@sstringa);
|
|
$sstring = '/'.$sstring;
|
|
}
|
|
my $ref = ref $html;
|
|
my $retstr;
|
|
if($ref) {
|
|
#This means the http parser knows what to do and we're working on a parseable document
|
|
my $results = $html->getElementsByTagName('a');
|
|
my @nodes = $results->get_nodelist;
|
|
foreach my $context(@nodes) {
|
|
my $href = $context->getAttribute('href');
|
|
next unless($href);
|
|
if($href =~ m/$sstring/) {
|
|
if($mode eq 'dlstring') {
|
|
$retstr = (split(/$sstring/, $href,2))[1];
|
|
} elsif($mode eq 'vstring') {
|
|
if($sstring =~ m/downloads/) { #hack for elvui
|
|
$retstr = $context->getAttribute('href');
|
|
$retstr =~ s/$sstring//g;
|
|
} else {
|
|
$retstr = $context->getAttribute('data-name');
|
|
}
|
|
}
|
|
return $retstr if($retstr);
|
|
}
|
|
}
|
|
} else {
|
|
#This means we're on our own = whatever we're getting here is a html document as a string, unparsed.
|
|
my $parser = HTML::HTML5::Parser->new();
|
|
if(defined($html)) {
|
|
my @file = split(/\n/, $html);
|
|
foreach my $line(@file) {
|
|
if($line =~ m/$sstring/) {
|
|
my $parsed = $parser->parse_balanced_chunk($line);
|
|
my @nodes = $parsed->nonBlankChildNodes();
|
|
foreach my $node(@nodes) {
|
|
my @atr = $node->attributes();
|
|
if($mode eq 'dlstring') {
|
|
my $href = $node->getAttribute('href');
|
|
$retstr = (split(/$sstring/, $href,2))[1];
|
|
} elsif($mode eq 'vstring') {
|
|
if($sstring =~ m/downloads/) { #ElvUI hacks
|
|
$retstr = $node->getAttribute('href');
|
|
$retstr =~ s/$sstring//g;
|
|
} else {
|
|
$retstr = $node->getAttribute('data-name');
|
|
}
|
|
}
|
|
return $retstr if($retstr);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
return undef;
|
|
}
|
|
}
|
|
return undef;
|
|
}
|
|
|
|
sub download_update {
|
|
my $uri = shift;
|
|
my $fileid = shift;
|
|
my ($ret,$filename,$file) = pcurse::download($uri);
|
|
return (1,$filename,$file) if($ret);
|
|
return (0,undef,undef);
|
|
}
|
|
|
|
sub update {
|
|
my $filename = shift;
|
|
my $file = shift;
|
|
my $targetpath = shift;
|
|
$filename = '/tmp/'.$filename;
|
|
unless(-e "$filename") {
|
|
open my $fh, '>', "$filename" or return 0;
|
|
print $fh $file;
|
|
close $fh;
|
|
}
|
|
if(-e "$filename") {
|
|
my $ae = Archive::Extract->new(archive => "$filename");
|
|
if($ae->extract(to=>$targetpath)) {
|
|
system("rm","-v","$filename");
|
|
return 1;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
sub download {
|
|
my $uri = shift;
|
|
my $file = pcurse::http_get($uri);
|
|
if(defined($file)) {
|
|
my $filename = $file->filename;
|
|
my $content = $file->decoded_content;
|
|
return (1,$filename,$content);
|
|
} else {
|
|
return (0,$uri,undef);
|
|
}
|
|
}
|
|
|
|
sub init_pool {
|
|
my $w = shift;
|
|
my $p = Thread::Pool->new( {
|
|
workers => $w,
|
|
do => sub {
|
|
my $todo = shift;
|
|
if($todo eq 'check') {
|
|
my $addon = shift;
|
|
my $conf = shift;
|
|
my $html = pcurse::html_get($addon->{'host'}.$addon->{'uri'});
|
|
my $fileid = pcurse::get_latest_file_id($html,$addon->{'host'}.$addon->{'uri'});
|
|
if($fileid) {
|
|
$addon->{'fileid'} = $fileid;
|
|
my $version = pcurse::get_product_version($html,$addon->{'uri'},$fileid);
|
|
if($version && ($version ne $addon->{'version'})) {
|
|
unless($conf->{'test'}) {
|
|
if($fileid eq 'elvui') {
|
|
$addon->{'downloaduri'} = $addon->{'host'}.'/downloads/'.$version;
|
|
} else {
|
|
$addon->{'downloaduri'} = $addon->{'host'}.$addon->{'uri'}.'/download/'.$fileid.'/file';
|
|
}
|
|
$addon->{'targetversion'} = $version;
|
|
return { retval => 1, did => 'check', addon => $addon };
|
|
}
|
|
} elsif (! defined($version)) {
|
|
return { retval => 0, did => 'check', result => 'Could not find version number', addon => $addon };
|
|
} else {
|
|
return { retval => 0, did => 'check', result => 'No need to update', addon => $addon };
|
|
}
|
|
} else {
|
|
return { retval => 0, did => 'check', result => 'Could not find file id for '.$addon->{'name'} };
|
|
}
|
|
} elsif($todo eq 'download') {
|
|
my $uri = shift;
|
|
my $fileid = shift;
|
|
my ($ret,$filename,$file) = pcurse::download_update($uri,$fileid);
|
|
return { retval => $ret, did => 'download', filename => $filename, filecontent => $file } if($ret);
|
|
return { retval => 0, did => 'download', filename => undef, filecontent => undef, uri => $uri };
|
|
} else {
|
|
return { retval => 0, result => 'Unknown task' };
|
|
}
|
|
},
|
|
});
|
|
return $p;
|
|
}
|
|
|
|
sub updatelog {
|
|
my $now = DateTime->now->iso8601();
|
|
my $addon_name = shift;
|
|
my $addon_oldv = shift;
|
|
my $addon_newv = shift;
|
|
my $filename = $ENV{'HOME'}.'/.pcurse/update.log';
|
|
open my $fh, '>>', $filename, or return { retval => 0, message => 'Could not open '.$filename.' for appending' };
|
|
print $fh $now.': '.$addon_name.': '.$addon_oldv.' => '.$addon_newv."\n";
|
|
close $fh;
|
|
return { retval => 1 }
|
|
}
|
|
|
|
1;
|