1
0

tfnzbperl.pl 112 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250
  1. #!/usr/bin/perl -w
  2. ################################################################################
  3. # $Id: tfnzbperl.pl 3301 2007-12-07 00:29:38Z danez $
  4. # $Date: 2007-12-06 18:29:38 -0600 (Thu, 06 Dec 2007) $
  5. # $Revision: 3301 $
  6. ################################################################################
  7. # #
  8. # Copyright (C) 2004 jason plumb #
  9. # #
  10. # This program is free software; you can redistribute it and/or #
  11. # modify it under the terms of the GNU General Public License #
  12. # as published by the Free Software Foundation; either version 2 #
  13. # of the License, or (at your option) any later version. #
  14. # #
  15. # This program is distributed in the hope that it will be useful, #
  16. # but WITHOUT ANY WARRANTY; without even the implied warranty of #
  17. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
  18. # GNU General Public License for more details. #
  19. # #
  20. # You should have received a copy of the GNU General Public License #
  21. # along with this program; if not, write to the Free Software #
  22. # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. #
  23. # #
  24. ################################################################################
  25. # #
  26. # nzbperl.pl -- version 0.6.8 #
  27. # #
  28. # for more information: #
  29. # http://noisybox.net/computers/nzbperl/ #
  30. # #
  31. # this version is modified and extended for torrentflux-b4rt #
  32. # http://tf-b4rt.berlios.de/ #
  33. # #
  34. ################################################################################
  35. # #
  36. # Required : #
  37. # * IO::File #
  38. # * IO::Select #
  39. # * IO::Socket::INET #
  40. # * File::Basename #
  41. # * Getopt::Long #
  42. # * Cwd #
  43. # * XML::Simple #
  44. # * XML::DOM #
  45. # #
  46. # Optional : #
  47. # * threads #
  48. # * Thread::Queue #
  49. # #
  50. ################################################################################
  51. use strict;
  52. use File::Basename;
  53. use IO::File;
  54. use IO::Select;
  55. use XML::DOM;
  56. use Getopt::Long;
  57. use Time::HiRes qw(gettimeofday tv_interval); # timer stuff
  58. use Cwd;
  59. use FluxCommon;
  60. use StatFile;
  61. ################################################################################
  62. ################################################################################
  63. # fields #
  64. ################################################################################
  65. my $version = '0.6.8';
  66. #my $ospeed = 9600;
  67. my $recv_chunksize = 5*1024; # How big of chunks we read at once from a connection (this is pulled from ass)
  68. my $UPDATE_URL = 'http://noisybox.net/computers/nzbperl/nzbperl_version.txt';
  69. my $dispchunkct = 250; # Number of data lines to read between screen updates.
  70. my $targkBps = 0;
  71. my ($medbw, $lowbw) = (95, 35); # Defaults for low and medium speed settings.
  72. my $sleepdur = 0; # Used when throttling
  73. # Make stdout not buffered.
  74. my $old_fh = select(STDOUT);
  75. $| = 1;
  76. select($old_fh);
  77. my $quitnow = 0;
  78. my $showinghelpscreen = 0;
  79. my $skipthisfile = 0;
  80. my $usecolor = 1;
  81. # These are getting hefty, so they're now 5 per line
  82. my ( $server, $port, $user, $pw, $keepparts,
  83. $keepbroken, $keepbrokenbin, $help, $nosort, $overwritefiles,
  84. $connct, $nocolor, $insane, $dropbad, $skipfilect,
  85. $reconndur, $filterregex, $configfile, $uudeview, $daemon,
  86. $dlrelative, $dlpath, $noupdate, $ssl, $socks_server,
  87. $socks_port, $proxy_user, $proxy_passwd, $http_proxy_server, $http_proxy_port,
  88. $dlcreate, $dlcreategrp, $noansi, $queuedir, $rcport,
  89. $postDecProg, $postNzbProg, $ipv6, $forever, $DECODE_DBG_FILE,
  90. $ifilterregex, $dthreadct, $diskfree, $tfuser
  91. ) =
  92. ( '', -1, '', '', 0,
  93. 0, 0, 0, 0, 0,
  94. 2, 0, 0, 0, 0,
  95. 300, undef, "$ENV{HOME}/.nzbperlrc", undef, 0,
  96. undef, undef, 0, undef, undef,
  97. -1, undef, undef, undef, -1,
  98. undef, undef, 0, undef, undef,
  99. undef, undef, undef, undef, undef,
  100. undef, 1, undef, ''
  101. );
  102. # How commandline args are mapped to vars. This map is also used by config file processor
  103. my %optionsmap = ('server=s' => \$server, 'user=s' => \$user, 'pw=s' => \$pw,
  104. 'help' => \$help, 'med=s' => \$medbw, 'low=s' => \$lowbw,
  105. 'speed=s' => \$targkBps, 'keepparts' => \$keepparts,
  106. 'keepbroken' => \$keepbroken, 'keepbrokenbin' => \$keepbrokenbin,
  107. 'nosort' => \$nosort, 'redo' => \$overwritefiles, 'conn=i' => \$connct,
  108. 'nocolor' => \$nocolor, 'insane' => \$insane, 'dropbad' => \$dropbad,
  109. 'skip=i' => \$skipfilect, 'retrywait=i' => \$reconndur, 'filter=s' => \$filterregex,
  110. 'config=s' => \$configfile, 'uudeview=s' => \$uudeview, 'dlrelative' => \$dlrelative,
  111. 'dlpath=s' => \$dlpath, 'noupdate' => \$noupdate, 'ssl' => \$ssl,
  112. 'socks_server=s' => \$socks_server, 'socks_port=i' => \$socks_port,
  113. 'socks_user=s' => \$proxy_user, 'socks_passwd=s' => \$proxy_passwd,
  114. 'http_proxy=s' => \$http_proxy_server, 'dlcreate'=>\$dlcreate,
  115. 'dlcreategrp' => \$dlcreategrp, 'noansi' => \$noansi, 'rcport=i' => \$rcport,
  116. 'postdec=s' => \$postDecProg, 'postnzb=s' => \$postNzbProg, 'ipv6' => \$ipv6,
  117. 'chunksize=s' => \$recv_chunksize, 'decodelog=s' => \$DECODE_DBG_FILE,
  118. 'ifilter=s' => \$ifilterregex, 'dthreadct=s' => \$dthreadct,
  119. 'diskfree=s' => \$diskfree, 'tfuser=s' => \$tfuser);
  120. ################################################################################
  121. # main #
  122. ################################################################################
  123. # parse args
  124. if (defined(my $errmsg = handleCommandLineOptions())) {
  125. showUsage($errmsg);
  126. exit 1;
  127. }
  128. # message
  129. printMessage("nzbperl starting up...\n");
  130. # ipv6
  131. if (not $ipv6){
  132. use IO::Socket::INET;
  133. }
  134. # Verify that uudeview is installed
  135. if (not haveUUDeview()){
  136. printError("Please install and configure uudeview and try again.\n");
  137. exit;
  138. }
  139. if (!($uudeview =~ m#^([\w\s\.\_\-\/\\]+)$#)) {
  140. printError("Invalid characters in uudeview path.\n");
  141. exit;
  142. }
  143. # some vars
  144. my $lastDirCheckTime = 0;
  145. my $lastDiskFullTime = undef;
  146. my $lastDiskFreePerc = 0;
  147. my %nzbfiles; # the hash/queue of nzb files we're handling
  148. # $nzbfiles{'files'}->{<filename>}->{'read'} : 1 if we've parsed/loaded it
  149. # $nzbfiles{'files'}->{<filename>}->{'finished'} : 1 if all files have been downloaded
  150. # ui-vars
  151. my ($oldwchar, $wchar, $oldhchar, $hchar, $wpixels, $hpixels) = (0); # holds screen size info
  152. my @lastdrawtime = Time::HiRes::gettimeofday();
  153. # statusmessages
  154. my @statusmsgs;
  155. # file-vars
  156. my ($file_nzb, $file_stat, $file_cmd, $file_pid);
  157. # fileset
  158. my @fileset;
  159. if (scalar(@ARGV) > 0){
  160. $file_nzb = shift @ARGV; #$ARGV[0];
  161. my @fsparts = parseNZB($file_nzb, 1);
  162. if (!defined($fsparts[0])) {
  163. printError("No Fileset-Parts found. exit.");
  164. exit;
  165. }
  166. @fsparts = regexAndSkipping(@fsparts); # It checks options inside too
  167. push @fileset, @fsparts;
  168. #
  169. $file_stat = $file_nzb.".stat";
  170. $file_cmd = $file_nzb.".cmd";
  171. $file_pid = $file_nzb.".pid";
  172. }
  173. my @queuefileset = @fileset;
  174. # message
  175. printMessage('Looks like we have got ' . scalar @fileset . ' possible files ahead of us.'."\n");
  176. # suspectFileInd
  177. my @suspectFileInd;
  178. if ($insane) {
  179. } else{
  180. &doNZBSanityChecks();
  181. if($dropbad){
  182. &dropSuspectFiles();
  183. }
  184. }
  185. # totals
  186. my %totals = (
  187. 'total size' => computeTotalNZBSize(@fileset),
  188. 'finished files' => 0,
  189. 'total bytes' => 0,
  190. 'total file ct' => scalar @fileset
  191. );
  192. my %totalsCopy = (
  193. 'total size' => $totals{'total size'},
  194. 'finished files' => $totals{'finished files'},
  195. 'total bytes' => $totals{'total bytes'},
  196. 'total file ct' => $totals{'total file ct'}
  197. );
  198. # startup-message
  199. printMessage("nzbperl starting up :\n");
  200. printMessage(" - files : ".$totals{'total file ct'}."\n");
  201. printMessage(" - size : ".$totals{'total size'}."\n");
  202. printMessage(" - tfuser : ".$tfuser."\n");
  203. printMessage(" - nzbfile : ".$file_nzb."\n");
  204. printMessage(" - statfile : ".$file_stat."\n");
  205. printMessage(" - pidfile : ".$file_pid."\n");
  206. printMessage(" - cmdfile : ".$file_cmd."\n");
  207. printMessage(" - dlpath : ".$dlpath."\n");
  208. printMessage(" - server : ".$server."\n");
  209. printMessage(" - speed : ".$targkBps."\n");
  210. printMessage(" - conn : ".$connct."\n");
  211. printMessage(" - dthreadct : ".$dthreadct."\n");
  212. # sf-instance-field (reuse object)
  213. my $sf = StatFile->new($file_stat);
  214. # write af
  215. writeStatStartup();
  216. # write pid
  217. pidFileWrite();
  218. # Check for and delete stale .cmd files
  219. if (-e $file_cmd ) {
  220. printMessage("removing command-file ".$file_cmd."...\n");
  221. unlink($file_cmd);
  222. }
  223. # set up our signal handlers
  224. printMessage("setting up signal handlers...\n");
  225. $SIG{HUP} = \&gotSigHup;
  226. $SIG{INT} = \&gotSigInt;
  227. $SIG{TERM} = \&gotSigTerm;
  228. $SIG{QUIT} = \&gotSigQuit;
  229. # start remote control
  230. my $rc_sock = undef;
  231. my @rc_clients;
  232. #startRemoteControl();
  233. my @conn;
  234. createNNTPConnections();
  235. if ($user){
  236. unless (doLogins()) {
  237. printError("Error authenticating to server.\nPlease check the user/pass info and try again.\n");
  238. # shutdown
  239. shutdownClient();
  240. }
  241. }
  242. # Start up the decoding thread(s)...
  243. my ($decMsgQ, $decQ, @decThreads);
  244. if (usingThreadedDecoding()){
  245. $decMsgQ = Thread::Queue->new; # For status msgs
  246. $decQ = Thread::Queue->new;
  247. foreach my $i (1..$dthreadct){
  248. push @decThreads, threads->new(\&file_decoder_thread, $i);
  249. }
  250. }
  251. # message
  252. printMessage("nzbperl up and running.\n");
  253. # main loop
  254. my @dlstarttime = Time::HiRes::gettimeofday();
  255. my %lasttime = (
  256. 'stat' => [gettimeofday],
  257. 'cmd' => [gettimeofday]
  258. );
  259. my $noMoreWorkTodo = 0;
  260. my $elapsed = 0;
  261. while (1) {
  262. # nzb-action
  263. doFileAssignments();
  264. doBodyRequests();
  265. doReceiverPart();
  266. # if 5 secs passed, write stat-file
  267. $elapsed = tv_interval($lasttime{'stat'});
  268. if ($elapsed >= 5) {
  269. # stat-file
  270. writeStatRunning();
  271. # set time
  272. $lasttime{'stat'} = [gettimeofday];
  273. }
  274. # if 1 sec passed, process command-stack
  275. $elapsed = tv_interval($lasttime{'cmd'});
  276. if ($elapsed >= 1) {
  277. # process command stack
  278. processCommandStack();
  279. # set time
  280. $lasttime{'cmd'} = [gettimeofday];
  281. }
  282. # queueNewNZBFilesFromDir(); # queue up new nzb files from dir (guards inside)
  283. # See if queuefileset is empty AND all sockets don't have files
  284. # when that happens, that's when we're done.
  285. # if(not scalar @queuefileset){ # no more files in queue
  286. # doBodyRequests(); # total hack, but that's where decoding happens...
  287. # }
  288. # dequeueNextNZBFileIfNecessary();
  289. # remote controls
  290. #doRemoteControls();
  291. # exit on quit
  292. $quitnow and last;
  293. # check if done
  294. $noMoreWorkTodo = no_more_work_to_do();
  295. if ($noMoreWorkTodo) {
  296. # done
  297. printMessage("all downloads complete.\n");
  298. last;
  299. }
  300. }
  301. # message
  302. printMessage("nzbperl shutting down...\n");
  303. # Do some cleanups
  304. if ($quitnow) {
  305. foreach my $c (@conn){
  306. next unless $c->{'file'};
  307. if($c->{'tmpfile'}){
  308. printMessage("Closing and deleting " . $c->{'tmpfilename'} . "...\n");
  309. undef $c->{'tmpfile'}; # causes a close
  310. unlink $c->{'tmpfilename'};
  311. }
  312. }
  313. }
  314. # Clean up server socket for remote control schtuff
  315. #disconnectAll();
  316. # decoder-threads
  317. printMessage("Waiting for file decoding thread(s) to terminate...\n");
  318. eval {
  319. local $SIG{ALRM} = sub {die "alarm\n"};
  320. alarm 30;
  321. #
  322. foreach my $i (1..$dthreadct){
  323. # Send a quit now message for each decoder thread.
  324. usingThreadedDecoding() and $decQ->enqueue('quit now');
  325. }
  326. foreach my $i (0..$dthreadct-1){
  327. # Now join on every decoder thread, waiting for all to finish
  328. usingThreadedDecoding() and $decThreads[$i]->join;
  329. }
  330. #
  331. alarm 0;
  332. };
  333. # Check for alarm (timeout) condition
  334. if ($@) {
  335. printMessage("possible hung thread(s), waited 30 secs for file decoding thread(s) :\n ".$@."\n");
  336. }
  337. # shutdown
  338. shutdownClient();
  339. ################################################################################
  340. # subs #
  341. ################################################################################
  342. # Descriptions of what's in the connection hash (for sanity sake)
  343. #
  344. # $conn->{'sock'} : the socket for comms
  345. # $conn->{'msg'} : message that describes what's going on
  346. # $conn->{'file'} : the file it's working on
  347. # $conn->{'segnum'} : the segment number of the file it's working on
  348. # $conn->{'segbytes'} : number of bytes read in the current segment
  349. # $conn->{'filebytes'} : number of bytes read in the current file
  350. # $conn->{'bstatus'} : status about how we're handling a body (starting/finishing)
  351. # $conn->{'buff'} : where body data is buffered
  352. # $conn->{'tmpfilename'}: temporary file name
  353. # $conn->{'tmpfile'} : temporary file handle
  354. # $conn->{'bwstarttime'}: time when the bandwdith applied
  355. # $conn->{'bwstartbytes'}: bytes read on file when bandwidth applied
  356. # $conn->{'truefname'} : true filename on disk (assumed after decoding)
  357. # $conn->{'skipping'} : indicates we're in the middle of a skipping operation
  358. # $conn->{'last data'} : time when data was last seen on this channel
  359. # $conn->{'sleep start'}: time that we started sleeping (for retries)
  360. # $conn->{'isbroken'} : set when one or more parts fails to download
  361. # $conn->{'tfseq'} : temporary file sequence id
  362. #########################################################################################
  363. # no_more_work_to_do - returns 1 if there is more work to do, 0 otherwise. Used to
  364. # detect when the main loop body should terminate
  365. #########################################################################################
  366. sub no_more_work_to_do {
  367. foreach my $i (1..$connct){
  368. if($conn[$i-1]->{'file'}){
  369. return 0;
  370. }
  371. }
  372. return scalar(@queuefileset) == 0; # no more files in queue, all done
  373. }
  374. #########################################################################################
  375. # This is the thread that does file decoding
  376. # It uses two queues for communication -- decQ for files to decode, decMsgQ for status
  377. # messages back to the main thread.
  378. #########################################################################################
  379. sub file_decoder_thread {
  380. my $threadNum = shift;
  381. my ($nzbpath, $nzbfile, $isbroken, $islastonnzb, $tmpfilename, $truefilename, $decodedir);
  382. my $prefixMsg = ($dthreadct > 1) ? "Decoder #$threadNum:" : '';
  383. while(1){
  384. # We get 6 things on the q per file...
  385. # nzbpath, $nzbfile, isbroken, tmpfilename, truefilename, decodedir
  386. $nzbpath = $decQ->dequeue;
  387. last unless defined($nzbpath);
  388. ($nzbpath =~ /^quit now$/) and last; # Time to shut down
  389. $nzbfile = $decQ->dequeue;
  390. $isbroken= $decQ->dequeue;
  391. $islastonnzb = $decQ->dequeue;
  392. $tmpfilename = $decQ->dequeue;
  393. $decodedir = $decQ->dequeue;
  394. $truefilename = $decQ->dequeue;
  395. doUUDeViewFile($nzbpath, $nzbfile, $isbroken, $islastonnzb,
  396. $tmpfilename, $decodedir, $truefilename, $prefixMsg);
  397. }
  398. }
  399. #########################################################################################
  400. # Does multiplexed comms receiving
  401. #########################################################################################
  402. sub doReceiverPart {
  403. my $select = IO::Select->new();
  404. foreach my $i (1..$connct){
  405. next unless (defined($conn[$i-1]->{'sock'}));
  406. #next unless ($conn[$i-1]->{'file'});
  407. $select->add($conn[$i-1]->{'sock'});
  408. }
  409. # If there are no active connections, we need to do a little sleep to prevent maxing out cpu.
  410. # the select->can_read call passes right through if it has no handles.
  411. (not $select->count()) and select undef, undef, undef, 0.1;
  412. my @ready = $select->can_read(0.25);
  413. foreach my $i (1..$connct){
  414. my $conn = $conn[$i-1];
  415. #next unless $conn->{'file'}; # This connection must be working on a file...otherwise next
  416. # TODO: Create a way to disable reconnection (when we don't want to do it)
  417. # Reconnect if we don't have a socket but we do have a file.
  418. if((not defined($conn->{'sock'})) and $conn->{'file'}){
  419. doReconnectLogicPart($i-1);
  420. next;
  421. }
  422. my $canread = 0;
  423. foreach my $fh (@ready) {
  424. if (defined($conn->{'sock'}) and $fh == $conn->{'sock'}) {
  425. $canread = 1;
  426. last;
  427. }
  428. }
  429. if ($canread) {
  430. my ($recvret, $buff);
  431. if (ref($conn->{'sock'}) eq "IO::Socket::SSL") {
  432. $recvret = $conn->{'sock'}->sysread($buff, $recv_chunksize);
  433. }
  434. else {
  435. $recvret = recv($conn->{'sock'}, $buff, $recv_chunksize, 0);
  436. if(defined($recvret)){
  437. $recvret = length $buff;
  438. }
  439. else{
  440. $recvret = -1;
  441. }
  442. }
  443. if(($recvret < 0) or !length($buff)){
  444. # TODO: Determine how to gracefully handle the crap we've already downloaded
  445. if (ref($conn->{'sock'}) eq "IO::Socket::SSL") {
  446. $conn->{'sock'}->shutdown( 2 );
  447. $conn->{'sock'}->close( SSL_no_shutdown => 1 );
  448. }
  449. else {
  450. $conn->{'sock'}->close;
  451. }
  452. $conn->{'sock'} = undef;
  453. $conn->{'sleep start'} = time;
  454. statMsg(sprintf("* Remote disconnect on connection #%d", $i));
  455. drawStatusMsgs();
  456. $conn->{'buff'} = '';
  457. next;
  458. }
  459. $conn->{'buff'} .= $buff;
  460. if(not connIsStartingSeg($conn)){ # only bump these up if we're not starting...
  461. $conn->{'segbytes'} += length($buff);
  462. $conn->{'filebytes'} += length($buff);
  463. $totals{'total bytes'} += length($buff);
  464. }
  465. $conn->{'last data'} = time;
  466. # Spool all lines from the buffer into the output file.
  467. spoolOutConnBuffData($i, $conn);
  468. }
  469. #drawScreenAndHandleKeys();
  470. doThrottling();
  471. }
  472. if ($#ready < 0) {
  473. #drawScreenAndHandleKeys();
  474. doThrottling();
  475. }
  476. }
  477. #########################################################################################
  478. # spoolOutConnBuffData - spool the given connection's data to the output file.
  479. # There's other stuff here too....it should be made simpler.
  480. #########################################################################################
  481. sub spoolOutConnBuffData {
  482. my ($i, $conn) = @_;
  483. return unless defined($conn->{'buff'}) and
  484. length($conn->{'buff'}) and
  485. defined($conn->{'tmpfile'});
  486. while(1){
  487. my $ind1 = index $conn->{'buff'}, "\r\n";
  488. last unless $ind1 >= 0;
  489. my $line = substr $conn->{'buff'}, 0, $ind1+2, '';
  490. if(connIsStartingSeg($conn)){
  491. startSegmentOnConnection($i, $conn, $line);
  492. next;
  493. }
  494. # Try and detect the "real" filename
  495. if(not $conn->{'truefname'}){
  496. my $tfn = getTrueFilename($line);
  497. if($tfn){
  498. $conn->{'truefname'} = $tfn;
  499. statMsg("Conn. $i: Found true filename: $tfn");
  500. my $targdir = getDestDirForFile($conn->{'file'}); # where the file is going on disk
  501. makeTargetDirIfNecessary($targdir);
  502. my $tfndisk = $targdir . '/' . $tfn;
  503. if(-e $tfndisk){
  504. if(!$overwritefiles){
  505. # We can't just close and delete, because there will likely still be
  506. # data waiting in the receive buffer. As such, we have to set a flag
  507. # to indicate that the file already exists and should be skipped...
  508. # This is perhaps a bit silly -- we have to finish slurping in the
  509. # BODY part before we can start working on the next file...
  510. statMsg("Conn. $i: File already exists on disk (skipping after segment completes)");
  511. $conn->{'skipping'} = 1;
  512. }
  513. }
  514. }
  515. }
  516. if($line =~ /^\.\r\n/){ # detect end of BODY..
  517. $conn->{'bstatus'} = 'finished';
  518. if($conn->{'skipping'}){
  519. $totals{'total file ct'}--;
  520. $totals{'total bytes'} -= $conn->{'filebytes'}; # Remove bytes downloaded
  521. $totals{'total size'} -= $conn->{'file'}->{'totalsize'}; # Remove file bytes from job total
  522. undef $conn->{'tmpfile'}; # causes a close
  523. unlink $conn->{'tmpfilename'};
  524. $conn->{'file'} = undef;
  525. $conn->{'skipping'} = undef; # no longer skipping (for next time)
  526. }
  527. last;
  528. }
  529. else{
  530. $line =~ s/^\.\././o;
  531. print {$conn->{'tmpfile'}} $line;
  532. }
  533. }
  534. }
  535. #########################################################################################
  536. # Figures out where a file will be going on disk. Returns the directory.
  537. #########################################################################################
  538. sub getDestDirForFile {
  539. my $file = shift;
  540. if(defined($dlpath)){
  541. if (defined($dlcreate)) { # if we like to create nicely organized subdirs
  542. return $dlpath . $file->{'nzb path'};
  543. }
  544. elsif (defined($dlcreategrp)){
  545. return $dlpath . $file->{'groups'}->[0];
  546. }
  547. return $dlpath;
  548. }
  549. elsif(defined($dlrelative)){
  550. return $file->{'nzb path'};
  551. }
  552. return undef; #this should not happen...either dlpath or dlrelative should be set
  553. }
  554. #########################################################################################
  555. # makes the given dowload dir if necessary
  556. #########################################################################################
  557. sub makeTargetDirIfNecessary {
  558. my $targdir = shift;
  559. if( not -d ($targdir) and defined($dlpath) and
  560. (defined($dlcreate) or defined($dlcreategrp))){
  561. if(!mkdir($targdir)){
  562. statMsg("ERROR: Could not create $targdir: $!");
  563. }
  564. }
  565. }
  566. #########################################################################################
  567. # connIsStartingSeg - Returns 1 if the segment BODY is being started on a connection,
  568. # or returns 0 otherwise.
  569. #########################################################################################
  570. sub connIsStartingSeg {
  571. my $conn = shift;
  572. return (defined($conn) and
  573. defined($conn->{'bstatus'}) and
  574. ($conn->{'bstatus'} =~ /starting/));
  575. }
  576. #########################################################################################
  577. # startSegmentOnConnection - Handles an input line when a segment is just starting
  578. # on a connection. This looks into detecting missing segments and handles server
  579. # responses that mean various things.
  580. #########################################################################################
  581. sub startSegmentOnConnection {
  582. my ($i, $conn, $line) = @_;
  583. my ($mcode, $msize, $mbody, $mid) = split /\s+/, $line;
  584. # We're just starting, need to slurp up 222 (or other) response
  585. if($line =~ /^2\d\d\s.*\r\n/s){
  586. # Bad case where server sends a 5xx message after a 2xx (222)
  587. if(!$msize and ($conn->{'buff'} =~ /^5\d\d /)){
  588. # Handle this error condition (display message to user)
  589. my $errline = $conn->{'buff'};
  590. $errline =~ s/\r\n.*//s;
  591. statMsg(sprintf("Conn. %d: Server returned error: %s", $i, $errline));
  592. }
  593. else{
  594. $conn->{'segbytes'} = length($conn->{'buff'});
  595. }
  596. $conn->{'bstatus'} = 'running';
  597. }
  598. else{ # This is an error condition -- often when the server can't find a segment
  599. $line =~ s/\r\n$//;
  600. statMsg( sprintf("Conn. %d: FAILED to fetch part #%d (%s)", $i,
  601. $conn->{'segnum'}+1, $line));
  602. drawStatusMsgs();
  603. $conn->{'bstatus'} = 'finished'; # Flag BODY segment as finished
  604. $conn->{'isbroken'} = 1;
  605. # Ok, so now that a segment fetch FAILED, we need to determine how to continue...
  606. # We will look at the keep variables to determine how to continue...
  607. # If keepbroken or keepbrokenbin are set, we will keep downloading parts...otherwise we will bump
  608. # up the segnum so that we skip all remaining segments (if any)
  609. if($keepbroken or $keepbrokenbin){ # If we shound continue downloading this broken file
  610. # Subtract the size of the current segment from the totals
  611. # (for this file and for the grand totals)
  612. my $failedsegsize = @{$conn->{'file'}->{'segments'}}[$conn->{'segnum'}]->{'size'};
  613. $totals{'total size'} -= $failedsegsize ;
  614. $conn->{'file'}->{'totalsize'} -= $failedsegsize;
  615. }
  616. else{
  617. statMsg(sprintf("Conn. %d: Aborting file (failed to fetch segment #%d)",
  618. $i, $conn->{'segnum'}+1));
  619. # Adjust totals due to skipping failed file
  620. $totals{'total file ct'}--;
  621. $totals{'total bytes'} -= $conn->{'filebytes'}; # Remove bytes downloaded
  622. $totals{'total size'} -= $conn->{'file'}->{'totalsize'}; # Remove file bytes from job total
  623. $conn->{'segnum'} = scalar @{$conn->{'file'}->{'segments'}} - 1;
  624. undef $conn->{'tmpfile'}; # causes a close
  625. unlink $conn->{'tmpfilename'};
  626. $conn->{'file'} = undef;
  627. }
  628. }
  629. }
  630. #########################################################################################
  631. # Handles reconnection logic
  632. #########################################################################################
  633. sub doReconnectLogicPart {
  634. my $i = shift;
  635. my $forceNow = shift; # can be specified to force a reconnect right now
  636. my $conn = $conn[$i];
  637. if(not $forceNow){
  638. my $remain = $reconndur - (time - $conn->{'sleep start'});
  639. if($remain > 0){ # still sleeping
  640. return;
  641. }
  642. }
  643. #my $iaddr = inet_aton($server) || die "Error resolving host: $server";
  644. statMsg(sprintf("Connection #%d attempting reconnect to %s:%d...", $i+1, $server, $port));
  645. ($conn->{'sock'}, my $line) = createSingleConnection($i, "$server:$port", 1);
  646. if(!$conn->{'sock'}){ # couldn't reconnect
  647. statMsg($line);
  648. $conn->{'sleep start'} = time; # reset reconnect timer
  649. return;
  650. }
  651. my $msg = sprintf("Connection #%d reestablished.", $i+1);
  652. $user and $msg .= "..performing login";
  653. statMsg($msg);
  654. drawStatusMsgs();
  655. if($user){ #need to authenticate...
  656. doSingleLogin($i, 1);
  657. statMsg(sprintf("Login on connection #%d complete.", $i+1));
  658. }
  659. $conn->{'sleep start'} = undef;
  660. # These two lines reset our state so that we restart the segment we were on
  661. # prior to the disconnect. Sure, a bit convoluted, but it's used elsewhere.
  662. $conn->{'bstatus'} = 'finished';
  663. defined($conn->{'segnum'}) and $conn->{'segnum'}--
  664. unless $conn->{'segnum'} < 0;;
  665. }
  666. #########################################################################################
  667. # Heuristically determines the "true" filename. Returns filename or undef
  668. #########################################################################################
  669. sub getTrueFilename {
  670. my $line = shift;
  671. $line =~ s/\s+$//;
  672. if($line =~ /^=ybegin/){ # Yenc candidate
  673. # I'm assuming that the last tag on the line is "name=...", which I honestly have no idea
  674. # if that's always true. :)
  675. $line =~ s/.* name=//;
  676. return $line;
  677. }
  678. elsif($line =~ /^begin \d+ /){ # UUencoded candidate
  679. $line =~ s/^begin \d+ //;
  680. return $line;
  681. }
  682. else{
  683. return undef;
  684. }
  685. }
  686. #########################################################################################
  687. # Handles segments and detects when we're done with a file
  688. #########################################################################################
  689. sub doBodyRequests {
  690. foreach my $i (1..$connct){
  691. my $conn = $conn[$i-1];
  692. my $file = $conn->{'file'};
  693. next unless $file; # Bail if we don't have a file
  694. if($conn->{'segnum'} < 0){
  695. next unless $conn->{'sock'}; # no socket, perhaps waiting for reconnect
  696. $conn->{'segnum'} = 0;
  697. my $seg = @{$file->{'segments'}}[0];
  698. #$conn->{'seg'} = $seg;
  699. my $msgid = $seg->{'msgid'};
  700. sockSend($conn->{'sock'}, 'BODY <' . $msgid . ">\r\n");
  701. $conn->{'bstatus'} = 'starting';
  702. $conn->{'segbytes'} = 0;
  703. }
  704. elsif($conn->{'bstatus'} =~ /finished/){ # finished a segment
  705. $conn->{'segnum'}++;
  706. if($conn->{'segnum'} >= scalar @{$file->{'segments'}}){ # All segments for this file exhausted.
  707. cursorPos(5, 6+(3*($i-1)));
  708. my $len = pc("File finished! Sending details to decoder queue...\n", 'bold white');
  709. #print ' ' x ($wchar-$len-6);
  710. statMsg("Conn. $i: Finished downloading " . $conn->{'file'}->{'name'});
  711. doDecodeOrQueueCompletedFile($conn);
  712. drawStatusMsgs();
  713. $totals{'finished files'}++;
  714. $conn->{'file'} = undef;
  715. #$conn->{'seg'} = undef;
  716. }
  717. else {
  718. next unless $conn->{'sock'}; # no socket, perhaps waiting for reconnect
  719. my $segnum = $conn->{'segnum'};
  720. my $seg = @{$file->{'segments'}}[$segnum];
  721. #$conn->{'seg'} = $seg;
  722. my $msgid = $seg->{'msgid'};
  723. sockSend($conn->{'sock'}, 'BODY <' . $msgid . ">\r\n");
  724. $conn->{'bstatus'} = 'starting';
  725. $conn->{'segbytes'} = 0;
  726. }
  727. }
  728. }
  729. }
  730. #########################################################################################
  731. # doStartFileDecoding - initiates or performs a decode for a completed file.
  732. # If dthreadct == 0, this will decode in place, otherwise it just queues the request
  733. # to decode to the decoder thread.
  734. #########################################################################################
  735. sub doDecodeOrQueueCompletedFile {
  736. my $conn = shift;
  737. my $file = $conn->{'file'};
  738. undef $conn->{'tmpfile'}; # causes a close
  739. my $outdir = getDestDirForFile($file);
  740. $outdir = cwd unless defined($outdir); # default to current dir
  741. my $tmpfilename = $conn->{'tmpfilename'};
  742. my $truefilename = $conn->{'truefname'};
  743. my $isbroken = $conn->{'isbroken'};
  744. $isbroken = 0 unless (defined($isbroken)); # ensure a definite value
  745. my $islastonnzb = $file->{'lastonnzb'};
  746. if(usingThreadedDecoding()){
  747. # Queue the items to the decoding thread
  748. $decQ->enqueue($file->{'nzb path'}, $file->{'nzb file'},
  749. $isbroken, $islastonnzb, $tmpfilename, $outdir, $truefilename);
  750. }
  751. else{
  752. doUUDeViewFile($file->{'nzb path'}, $file->{'nzb file'},
  753. $isbroken, $islastonnzb, $tmpfilename, $outdir, $truefilename);
  754. }
  755. }
  756. #########################################################################################
  757. # Decodes a file to disk and handles cleanup (deleting/keeping parts)
  758. #########################################################################################
  759. sub doUUDeViewFile {
  760. my ($nzbpath, $nzbfile, $isbroken, $islastonnzb, $tmpfilename,
  761. $decodedir, $truefilename, $prefixMsg) = @_;
  762. $prefixMsg = '' unless defined($prefixMsg);
  763. $prefixMsg =~ s/\s+$//;
  764. length($prefixMsg) and $prefixMsg .= ' ';
  765. statOrQ($prefixMsg . "Starting decode of $truefilename");
  766. # Do the decode and confirm that it worked...
  767. if(!$isbroken or $keepbrokenbin){
  768. my $kb = '';
  769. $keepbrokenbin and $kb = '-d'; # If keeping broken, pass -d (desparate mode) to uudeview
  770. my $decodelogpart = '';
  771. my $qopts = '-q';
  772. if(defined($DECODE_DBG_FILE)){
  773. $decodelogpart = " >> $DECODE_DBG_FILE";
  774. $qopts = '-n';
  775. }
  776. else{
  777. $decodelogpart = " > /dev/null";
  778. }
  779. my $rc = system("$uudeview -i -a $kb $qopts \"$tmpfilename\" -p \"$decodedir\"$decodelogpart 2>&1");
  780. $rc and $isbroken = 1; # If decode failed, file is broken
  781. if($rc){ # Problem with the decode
  782. if(defined($DECODE_DBG_FILE)){
  783. statOrQ($prefixMsg . "FAILED decode of $tmpfilename (see $DECODE_DBG_FILE for details)");
  784. }
  785. else{
  786. statOrQ($prefixMsg . "FAILED decode of $tmpfilename");
  787. statOrQ("Consider using --decodelog <file> to troubleshoot.");
  788. }
  789. }
  790. else{
  791. statOrQ($prefixMsg . "Completed decode of " . $truefilename);
  792. }
  793. }
  794. # Decide if we need to keep or delete the temp .parts file
  795. if($keepparts or ($isbroken and $keepbroken)){
  796. my $brokemsg = $isbroken ? ' broken' : '';
  797. statOrQ("Keeping$brokemsg file segments in $tmpfilename (--keepparts given)");
  798. # TODO: rename to .broken
  799. }
  800. else {
  801. unlink($tmpfilename) or statOrQ("Error removing $tmpfilename from disk: $!");
  802. }
  803. runPostDecodeProgram($tmpfilename, $decodedir, $truefilename, $isbroken);
  804. $islastonnzb and (runPostNzbDecodeProgram($nzbpath, $nzbfile, $decodedir, $truefilename));
  805. }
  806. #########################################################################################
  807. # runPostDecodeProgram -- Possibly runs an external program after a file has been
  808. # decoded (regardless of success).
  809. #########################################################################################
  810. sub runPostDecodeProgram {
  811. my ($tmpfilename, $decodedir, $truefilename, $isbroken) = @_;
  812. return unless defined($postDecProg);
  813. $truefilename = $decodedir .
  814. (($decodedir =~ /\/$/) ? '' : '/') . $truefilename;
  815. runProgWithEnvParams($postDecProg, 'post-decoding',
  816. NZBP_FILE => $truefilename, NZBP_TEMPFILE => $tmpfilename,
  817. NZBP_ISBROKEN => $isbroken);
  818. }
  819. #########################################################################################
  820. # runPostNzbDecodeProgram -- Possibly runs external prog when nzb is completed.
  821. #########################################################################################
  822. sub runPostNzbDecodeProgram {
  823. my ($nzbpath, $nzbfile, $decodedir, $truefilename) = @_;
  824. return unless defined($postNzbProg); #option not specified
  825. runProgWithEnvParams($postNzbProg, 'post-nzb',
  826. NZBP_NZBDIR => $nzbpath, NZBP_NZBFILE => $nzbfile,
  827. NZBP_DECODEDIR => $decodedir, NZBP_LASTFILE => $truefilename);
  828. }
  829. #########################################################################################
  830. # Runs a program with environment vars prepended to the commandline as parameters.
  831. # This is used by the post decoder program runner and the post nzb program runner.
  832. #########################################################################################
  833. sub runProgWithEnvParams {
  834. my ($prog, $desc, %env) = @_;
  835. my $cmd = '';
  836. # This is a little strange...but showing the env vars onto the command is
  837. # the only way I could find to pass environments from a perl thread to
  838. # an external prog. I wish there was a better way (like using $ENV, but
  839. # that fails)
  840. foreach my $k (keys %env){
  841. my $envitem = $env{$k};
  842. $envitem =~ s/"/\\"/g; # escape double quotes
  843. $envitem =~ s/`/\\`/g; # escape backticks (evil)
  844. $cmd .= sprintf("export %s=\"%s\"; ", $k, $envitem);
  845. }
  846. $cmd .= $prog;
  847. statMsg("Running $desc program : $prog");
  848. system($cmd);
  849. statMsg("Finished running $desc program.");
  850. drawStatusMsgs();
  851. }
  852. #########################################################################################
  853. # Shifts from the file queue and assigns the files to a connection. When a file is
  854. # assigned, the first segment is not assigned.
  855. #########################################################################################
  856. sub doFileAssignments {
  857. foreach my $i (1..$connct){
  858. my $conn = $conn[$i-1];
  859. next if $conn->{'file'}; # already working on a file
  860. if(hitDiskSpaceLimit($queuefileset[0])){ # Do free space checking if option set
  861. next;
  862. }
  863. my $file = shift @queuefileset;
  864. last unless $file;
  865. statMsg(sprintf("Conn. %d: Starting file: %s", $i, $file->{'name'}));
  866. $conn->{'file'} = $file;
  867. $conn->{'segnum'} = -1;
  868. $conn->{'filebytes'} = 0;
  869. $conn->{'truefname'} = undef;
  870. $conn->{'bwstartbytes'} = 0;
  871. $conn->{'isbroken'} = 0; # Assume unbroken until we know it is
  872. @{$conn->{'bwstarttime'}} = Time::HiRes::gettimeofday();
  873. $conn->{'tfseq'}++;
  874. # Create temp filename and open
  875. my $tmpfile = 'nzbperl.tmp' . time . '.' . $i . '.' . $conn->{'tfseq'} . '.parts';
  876. if(defined($dlpath)){ # stick in dlpath if given
  877. $tmpfile = $dlpath . $tmpfile;
  878. }
  879. elsif(defined($dlrelative)){ # otherwise stick in relative dir to nzbfile
  880. $tmpfile = $file->{'nzb path'} . $tmpfile;
  881. if(not -w $file->{'nzb path'}){
  882. statMsg(sprintf("*** ERROR: nzb path %s is not writable! There will be failures!", $file->{'nzb path'}));
  883. statMsg("*** Please change the permissions or use --dlpath instead of --dlrelative.");
  884. }
  885. }
  886. ($tmpfile =~ m#^([\w\d\s\.\_\-\/\\]+)$#) and $tmpfile = $1; # untaint tmpfile
  887. $conn->{'tmpfilename'} = $tmpfile;
  888. $conn->{'tmpfile'} = undef; # just to be absolutely sure
  889. open $conn->{'tmpfile'}, ">$tmpfile" or
  890. (statMsg("*** ERROR opening $tmpfile (critical!)") and next);
  891. statMsg("Conn. $i: Opened temp file $tmpfile");
  892. binmode $conn->{'tmpfile'};
  893. }
  894. }
  895. #########################################################################################
  896. # Returns 1 if the param to prevent disk filling was set and we're within the threshhold
  897. #########################################################################################
  898. sub hitDiskSpaceLimit {
  899. return 0 unless defined $diskfree;
  900. my $file = shift;
  901. # Only check freespace every 15 seconds
  902. if(defined($lastDiskFullTime)){
  903. return 1 unless (time - $lastDiskFullTime) > 15;
  904. }
  905. my $freeperc = getFreeDiskPercentage(getDestDirForFile($file));
  906. if($freeperc <= $diskfree){
  907. if(not defined($lastDiskFullTime)){ # the first time we detect free space is out
  908. statMsg("Warning: Download disk has less than $diskfree% free.");
  909. statMsg("Waiting for free space before continuing downloading.");
  910. }
  911. $lastDiskFullTime = time;
  912. return 1;
  913. }
  914. $lastDiskFullTime = undef;
  915. return 0;
  916. }
  917. #########################################################################################
  918. # Tries to get the free disk percentage on the provided path
  919. #########################################################################################
  920. sub getFreeDiskPercentage {
  921. my $path = shift;
  922. my @reslines = `df '$path'`;
  923. my $line = pop @reslines;
  924. chomp $line;
  925. # Are all dfs created equal??? If not, we could use col headers?
  926. my ($fs, $size, $used, $avail, $dfperc, $mount) = split /\s+/, $line;
  927. $dfperc =~ s/%//;
  928. $lastDiskFreePerc = 100-$dfperc;
  929. return $lastDiskFreePerc;
  930. }
  931. #########################################################################################
  932. # Decides if its time to do the next nzb file...which is when the @queuefileset array
  933. # is empty and there is at least 1 idle connection.
  934. #########################################################################################
  935. sub dequeueNextNZBFileIfNecessary {
  936. return if scalar(@queuefileset); # still have queued files
  937. foreach my $i (1..$connct){
  938. if(not $conn[$i-1]->{'file'}){ # the connection is idle
  939. my ($newQueuedCt, $dequeuedNewFile, $reconnAttempts) = (0,0,0);
  940. $newQueuedCt = queueNewNZBFilesFromDir(1); # force a dircheck first
  941. $dequeuedNewFile = dequeueNextNZBFile();
  942. if($dequeuedNewFile){
  943. $reconnAttempts = reconnectAllDisconnectedNow();
  944. }
  945. if($newQueuedCt or $dequeuedNewFile or $reconnAttempts){
  946. drawStatusMsgs();
  947. }
  948. return;
  949. }
  950. }
  951. }
  952. #########################################################################################
  953. # Forces an immediate reconnect on all not connected connections.
  954. # Returns number of connections that had reconnect *attempts* (not necessarily the
  955. # number that were actually reconnected)
  956. #########################################################################################
  957. sub reconnectAllDisconnectedNow {
  958. my $retCt = 0;
  959. foreach my $i (1..$connct){
  960. if(not defined($conn[$i-1]->{'sock'})){
  961. doReconnectLogicPart($i-1, 1);
  962. $retCt++;
  963. }
  964. }
  965. return $retCt;
  966. }
  967. #########################################################################################
  968. # Pulls out the next nzb file in queue, parses it, and then add its files/parts to
  969. # @queuefileset.
  970. #########################################################################################
  971. sub dequeueNextNZBFile {
  972. my @keys = keys %{$nzbfiles{'files'}};
  973. foreach my $key (@keys){
  974. if(not $nzbfiles{'files'}->{$key}->{'read'}){
  975. statMsg("Moving to next nzb file in queue: $key");
  976. my @newset = parseNZB($queuedir . '/' . $key, 1);
  977. if(!defined($newset[0])){
  978. statMsg("Warning: no new files loaded from queued nzb file");
  979. return 0;
  980. }
  981. push @queuefileset, @newset;
  982. statMsg("Loaded " . scalar(@newset) . " new files to download from nzb file: $key");
  983. $totals{'total file ct'} += scalar @newset;
  984. $totals{'total size'} += computeTotalNZBSize(@newset);
  985. $nzbfiles{'files'}->{$key}->{'read'} = 1;
  986. return 1;
  987. }
  988. }
  989. return 0;
  990. }
  991. #########################################################################################
  992. # Looks at the nzbfile hash and counts the number that haven't been read (are queued)
  993. #########################################################################################
  994. sub countQueuedNZBFiles {
  995. my @keys = keys %{$nzbfiles{'files'}};
  996. my $ct = 0;
  997. foreach my $key (@keys){
  998. if($nzbfiles{'files'}->{$key}->{'read'} == 0){
  999. $ct++;
  1000. }
  1001. }
  1002. return $ct;
  1003. }
  1004. #########################################################################################
  1005. # queues new nzb files from the queue dir if they exist and adds them to the hash/queue
  1006. # of all nzb files we're processing. Returns the number of files dequeued.
  1007. #########################################################################################
  1008. sub queueNewNZBFilesFromDir {
  1009. my $forcecheck = shift;
  1010. return 0 unless $queuedir and not scalar @queuefileset;
  1011. return 0 unless $forcecheck or (time - $lastDirCheckTime > 15); # don't check more than once every 15 seconds
  1012. $lastDirCheckTime = time;
  1013. my $retCt = 0;
  1014. opendir(QDIR, $queuedir);
  1015. my @candidates = grep(/\.nzb$/, readdir(QDIR));
  1016. foreach my $file (@candidates){
  1017. if( !defined($nzbfiles{'files'}->{$file})){ # not queued yet
  1018. statMsg("Queueing new nzb file found on disk: $file");
  1019. $nzbfiles{'files'}->{$file}->{'read'} = 0;
  1020. $retCt++;
  1021. }
  1022. }
  1023. closedir(QDIR);
  1024. return $retCt;
  1025. }
  1026. #########################################################################################
  1027. # Start up the remote control(s)
  1028. #########################################################################################
  1029. sub startRemoteControl {
  1030. return unless defined($rcport); # nuthin to do
  1031. eval "use XML::Simple;";
  1032. ($@) and die "ERROR: XML::Simple required if using remote control...Please install it.";
  1033. $rc_sock = createRCMasterSocket();
  1034. printMessage("Remote control server socket created.\n");
  1035. }
  1036. #########################################################################################
  1037. # creates the remote control master port, using either ipv4 or ipv6
  1038. #########################################################################################
  1039. sub createRCMasterSocket {
  1040. my $ret;
  1041. my %opts = (Listen => 5, LocalAddr => 'localhost',
  1042. LocalPort => $rcport,
  1043. Proto=>'tcp', Type => SOCK_STREAM, Reuse => 1);
  1044. if($ipv6){
  1045. $ret = IO::Socket::INET6->new( %opts ) or die "Error creating remote control socket: $!";
  1046. }
  1047. else{
  1048. $ret = IO::Socket::INET->new( %opts ) or die "Error creating remote control socket: $!";
  1049. }
  1050. return $ret;
  1051. }
  1052. #########################################################################################
  1053. # Main loop entry point for handling remote control stuff
  1054. #########################################################################################
  1055. sub doRemoteControls {
  1056. return unless defined($rcport);
  1057. getNewRcClients();
  1058. handleRcClients();
  1059. my @tmprcc;
  1060. foreach my $client (@rc_clients){ #clean up dropped clients
  1061. if(defined($client->{'closenow'})){
  1062. statMsg(sprintf("Remote control client from %s:%s disconnected.", $client->{'ip'}, $client->{'port'}));
  1063. close $client->{'sock'};
  1064. }
  1065. else{
  1066. push @tmprcc, $client;
  1067. }
  1068. }
  1069. @rc_clients = @tmprcc;
  1070. }
  1071. #########################################################################################
  1072. # handleRcClients -- read and handle all remote commands from all clients
  1073. #########################################################################################
  1074. sub handleRcClients{
  1075. for (my $i=0; $i < scalar @rc_clients; $i++){
  1076. my $client = $rc_clients[$i];
  1077. my $cmd = readRcClientCommand($client);
  1078. defined($cmd) and handleRcClientCmd($client, $cmd);
  1079. }
  1080. }
  1081. #########################################################################################
  1082. # handleRcClientCmd - Handle's an rc client command
  1083. #########################################################################################
  1084. sub handleRcClientCmd {
  1085. my ($client, $cmdstr) = @_;
  1086. my ($cmd, $params, $responsemsg) = ($cmdstr, $cmdstr);
  1087. $cmd =~ s/\s+.*//;
  1088. $params =~ s/^\w+\s+//;
  1089. $params = '' unless $params ne $cmd;
  1090. if($cmd =~ /ping/i){
  1091. $responsemsg = sprintf("PONG! %s", $params);
  1092. }
  1093. elsif($cmd =~ /^quit/i){
  1094. sendRemoteResponse($client, "Nice having ya.");
  1095. $client->{'closenow'} = 1;
  1096. return;
  1097. }
  1098. elsif($cmd =~ /^keys/i){
  1099. my @keys = split //, $params;
  1100. foreach my $key (@keys){
  1101. handleKey($key);
  1102. }
  1103. $responsemsg = sprintf("Ok, processed %d keystrokes", scalar @keys);
  1104. }
  1105. elsif($cmd =~ /^summary/i){
  1106. $responsemsg = generateRcSummary();
  1107. }
  1108. elsif($cmd =~/^speed/i){
  1109. if($params =~ /\d+/){
  1110. $targkBps = $params;
  1111. $responsemsg = sprintf("Ok, set download speed to %dkBps", $params);
  1112. }
  1113. else{
  1114. $responsemsg = "Error: please specify speed in kBps";
  1115. }
  1116. }
  1117. elsif($cmd =~ /^diskfree/i){
  1118. $params =~ s/%//;
  1119. $diskfree = $params;
  1120. $responsemsg = "Ok, set max disk free percentage to $diskfree%";
  1121. }
  1122. elsif($cmd =~ /^enqueue/i){
  1123. if(defined($nzbfiles{'files'}->{$params})){ # not queued yet
  1124. $responsemsg = "Error: Refusing to queue file already queued ($params).";
  1125. }
  1126. elsif(not -e $params){
  1127. $responsemsg = "Error: File does not exist ($params)";
  1128. }
  1129. else{
  1130. $responsemsg = "Queueing new nzb file found on disk: $params";
  1131. statMsg($responsemsg);
  1132. $nzbfiles{'files'}->{$params}->{'read'} = 0;
  1133. }
  1134. }
  1135. else{
  1136. $responsemsg = "Sorry, command not understood.";
  1137. }
  1138. sendRemoteResponse($client, $responsemsg);
  1139. }
  1140. #########################################################################################
  1141. # sendRemoteResponse -- send a remote command response to the remote client.
  1142. #########################################################################################
  1143. sub sendRemoteResponse {
  1144. my ($client, $msg) = @_;
  1145. my $sock = $client->{'sock'};
  1146. # simple protocol, eh?
  1147. sockSend($sock, sprintf("%d\r\n%s\r\n", length($msg)+2, $msg));
  1148. }
  1149. #########################################################################################
  1150. # readRcClientCommand -- Attempts to read a command from the client socket
  1151. # returns the command or undef
  1152. #########################################################################################
  1153. sub readRcClientCommand {
  1154. my $client = shift;
  1155. my $buff = readNewRcClientSockData($client);
  1156. if(defined($buff)){
  1157. my $nlindex = index $client->{'data'}, "\r\n";
  1158. if($nlindex >= 0){
  1159. #get cmd and replace in client{data} with nothing
  1160. my $cmd = substr $client->{'data'}, 0, $nlindex+2, '';
  1161. $cmd = trimWS($cmd);
  1162. return $cmd;
  1163. }
  1164. }
  1165. return undef;
  1166. }
  1167. #########################################################################################
  1168. # readNewRcClientSockData -- Pulls client data off the socket if there is any.
  1169. #########################################################################################
  1170. sub readNewRcClientSockData {
  1171. my $client = shift;
  1172. my $sock = $client->{'sock'};
  1173. my $sockfn = fileno($sock);
  1174. my ($rin, $win, $ein) = ('', '', '');
  1175. my ($rout, $wout, $eout);
  1176. vec($rin, $sockfn, 1) = 1;
  1177. vec($win, $sockfn, 1) = 1;
  1178. vec($ein, $sockfn, 1) = 1;
  1179. my $nfound = select($rout=$rin, $wout=$win, $eout=$ein, 0);
  1180. return undef unless $nfound > 0;
  1181. if(vec($rout, $sockfn,1) == 1){
  1182. my $buff;
  1183. recv($sock, $buff, $recv_chunksize, 0);
  1184. if(not length($buff)){
  1185. $client->{'closenow'} = 1;
  1186. return undef;
  1187. }
  1188. $client->{'data'} .= $buff;
  1189. return $buff;
  1190. }
  1191. if((vec($eout, $sockfn, 1) == 1) || (vec($wout, $sockfn, 1) != 1)){
  1192. $client->{'closenow'} = 1;
  1193. }
  1194. return undef;
  1195. }
  1196. #########################################################################################
  1197. # Accepts new connections from clients and adds them to the list.
  1198. #########################################################################################
  1199. sub getNewRcClients {
  1200. while(1){
  1201. my ($rin,$rout) = ('','');
  1202. vec($rin, fileno($rc_sock), 1) = 1;
  1203. my $nfound = select($rout=$rin, undef, undef, 0);
  1204. last unless ($nfound > 0);
  1205. my $nclient;
  1206. my $client_addr = accept($nclient, $rc_sock);
  1207. #my $old = select($nclient);
  1208. #$| = 1; # make nonbuffered
  1209. #select($old);
  1210. my ($clientport, $clientippart) = sockaddr_in($client_addr);
  1211. my $clientip = inet_ntoa($clientippart);
  1212. statMsg("New remote control connection from " . $clientip . ":" . $clientport);
  1213. sockSend($nclient, "nzbperl version $version\r\n");
  1214. push @rc_clients, {'sock' => $nclient, 'ip' => $clientip, 'port' => $clientport};
  1215. }
  1216. }
  1217. #########################################################################################
  1218. # generateRcSummary - generates a summary of information for a remote request for it.
  1219. #########################################################################################
  1220. sub generateRcSummary {
  1221. my %s;
  1222. $s{'connections'} = $connct;
  1223. my $tspeed = $targkBps ? hrsv($targkBps*1024) . "Bps" : "unlimited";
  1224. $s{'speeds'} = {'current' => getCurrentSpeed(), 'target' => $tspeed, 'session' => getTotalSpeed()};
  1225. $s{'completed'} = {'files' => $totals{'finished files'}, 'size' => hrsv($totals{'total bytes'})};
  1226. $s{'completed'}->{'files'} = 0 unless $s{'completed'}->{'files'};
  1227. $s{'remaining'} = {'files' => $totals{'total file ct'}-$totals{'finished files'},
  1228. 'time' => getETA(), 'size' => hrsv($totals{'total size'} - $totals{'total bytes'}),
  1229. 'queued_nzb_files' => countQueuedNZBFiles()};
  1230. my $summary = XML::Simple->new()->XMLout(\%s, rootname => 'summary', noattr => 1);
  1231. $summary =~ s/\s+$//;
  1232. return $summary ;
  1233. }
  1234. #########################################################################################
  1235. # Creates all connections and adds them to the @conn global
  1236. #########################################################################################
  1237. sub createNNTPConnections {
  1238. foreach my $i (1..$connct){
  1239. #my $iaddr = inet_aton($server) || die "Error resolving host: $server";
  1240. my $paddr = "$server:$port";
  1241. ($conn[$i-1]->{'sock'}, my $line) = createSingleConnection($i-1, $paddr);
  1242. }
  1243. return 1;
  1244. }
  1245. #########################################################################################
  1246. # Connects to an NNTP server and attempts to read the greet string line.
  1247. # Returns the socket and the greet line.
  1248. #########################################################################################
  1249. sub createSingleConnection {
  1250. my ($i, $paddr, $silent) = @_;
  1251. my ($osock, $sock) = (undef, undef);
  1252. if (defined($socks_server)) {
  1253. porlp(sprintf("Attempting SOCKS connection #%d %s:%d ->%s:%d...",
  1254. $i+1, $socks_server, $socks_port, $server, $port), $silent);
  1255. my %sockparam = (ProxyAddr => $socks_server, ProxyPort => $socks_port,
  1256. ConnectAddr => $server, ConnectPort => $port );
  1257. if(defined($proxy_user)){ # Add authentication info
  1258. $sockparam{'AuthType'} = 'userpass';
  1259. $sockparam{'Username'} = $proxy_user;
  1260. $sockparam{'Password'} = $proxy_passwd;
  1261. }
  1262. $osock = IO::Socket::Socks->new( %sockparam );
  1263. }
  1264. elsif (defined($http_proxy_server)) {
  1265. porlp(sprintf('Attempting HTTP Proxy connection #%d %s:%d -> %s:%d...'."\n",
  1266. $i+1, $http_proxy_server, $http_proxy_port, $server, $port), $silent);
  1267. $osock = Net::HTTPTunnel->new( 'proxy-host' => $http_proxy_server,
  1268. 'proxy-port' => $http_proxy_port,
  1269. 'remote-host' => $server,
  1270. 'remote-port' => $port );
  1271. }
  1272. else {
  1273. porlp(sprintf('Attempting connection #%d to %s:%d...'."\n", $i+1, $server, $port), $silent);
  1274. $osock = createNNTPClientSocket($paddr);
  1275. }
  1276. if(!$osock){
  1277. porlp("Connection FAILED!\n", $silent);
  1278. return (undef, "Error connecting to server: '$!'");
  1279. }
  1280. porlp("success!\n", $silent);
  1281. if (defined($ssl)) {
  1282. porlp(sprintf("Establishing SSL connection #%d to %s:%d...\n", $i+1, $server, $port), $silent);
  1283. $sock = IO::Socket::SSL->start_SSL($osock);
  1284. die "SSL error: " . IO::Socket::SSL::errstr() . $! unless (defined($sock));
  1285. }
  1286. else {
  1287. $sock = $osock;
  1288. }
  1289. my $line = blockReadLine($sock); # read server connection/response string
  1290. not $line =~ /^(200|201)/ and die "Unexpected server response: $line" . "Expected 200 or 201.\n";
  1291. if (ref($sock) eq "IO::Socket::SSL") {
  1292. my ($subj, $iss, $cipher) = ($sock->peer_certificate("subject"),
  1293. $sock->peer_certificate("issuer"),
  1294. $sock->get_cipher());
  1295. pc("cipher: $cipher: Subject $subj: Issuer: $iss\n", "bold white");
  1296. }
  1297. return ($sock, $line);
  1298. }
  1299. #########################################################################################
  1300. # Encapsulates creating a socket for use with NNTP. Pulled to a sub because it can
  1301. # handle IPv6 sockets if the option is set.
  1302. #########################################################################################
  1303. sub createNNTPClientSocket {
  1304. my $paddr = shift;
  1305. my %opts = (PeerAddr => $paddr, Proto => 'tcp', Type => SOCK_STREAM);
  1306. $ipv6 and return IO::Socket::INET6->new(%opts);
  1307. return IO::Socket::INET->new(%opts);
  1308. }
  1309. #########################################################################################
  1310. # Attempts to perform a login on each connection
  1311. #########################################################################################
  1312. sub doLogins {
  1313. foreach my $i (1..$connct){
  1314. doSingleLogin($i-1);
  1315. }
  1316. return 1;
  1317. }
  1318. #########################################################################################
  1319. # Logs in a single connection. Pass in the connection index.
  1320. #########################################################################################
  1321. sub doSingleLogin {
  1322. my ($i, $silent) = @_;
  1323. my $conn = $conn[$i];
  1324. my $sock = $conn[$i]->{'sock'};
  1325. return unless $sock;
  1326. not $silent and printMessage(sprintf("Attempting login on connection #%d...\n", $i+1));
  1327. sockSend($sock, "AUTHINFO USER $user\r\n");
  1328. my $line = blockReadLine($sock);
  1329. if($line =~ /^381/){
  1330. sockSend($sock, "AUTHINFO PASS $pw\r\n");
  1331. $line = blockReadLine($sock);
  1332. $line =~ s/\r\n//;
  1333. (not $line =~ /^281/) and not $silent and printError(">FAILED<\n* Authentication to server failed: ($line)\n") and shutdownClient();
  1334. not $silent and printMessage("success!\n");
  1335. }
  1336. elsif($line =~ /^281/){ # not sure if this happens, but this means no pw needed I guess
  1337. not $silent and printMessage("no password needed, success!\n");
  1338. }
  1339. else {
  1340. not $silent and printError("server returned: $line\n");
  1341. printError(">LOGIN FAILED<\n");
  1342. # shutdown
  1343. shutdownClient();
  1344. }
  1345. }
  1346. #########################################################################################
  1347. # Computes and returns the total speed for this session.
  1348. #########################################################################################
  1349. sub getTotalSpeed {
  1350. my $runtime = Time::HiRes::tv_interval(\@dlstarttime);
  1351. return uc(hrsv($totals{'total bytes'}/$runtime)) . 'Bps';
  1352. }
  1353. #########################################################################################
  1354. # Looks at all the current connections and calculates a "current" speed
  1355. #########################################################################################
  1356. sub getCurrentSpeed {
  1357. my $sumbps = 0;
  1358. my $suppresshsrv = shift;
  1359. foreach my $i (1..$connct){
  1360. my $c = $conn[$i-1];
  1361. next unless $c->{'file'}; # skip inactive connections
  1362. $sumbps += ($c->{'filebytes'} - $c->{'bwstartbytes'})/Time::HiRes::tv_interval($c->{'bwstarttime'});
  1363. }
  1364. $suppresshsrv and return $sumbps;
  1365. return uc(hrsv($sumbps)) . 'Bps';
  1366. }
  1367. #########################################################################################
  1368. # gets the estimated ETA in hrs:mins:secs
  1369. #########################################################################################
  1370. {
  1371. my @old_speeds;
  1372. sub getETA {
  1373. my ($h, $m, $s);
  1374. my $curspeed = getCurrentSpeed(1) || 0; # in bytes/sec
  1375. if (push(@old_speeds, $curspeed) > 20) { # keep the last 20 measurements
  1376. shift(@old_speeds);
  1377. }
  1378. my $avgspeed = 0;
  1379. foreach my $i (@old_speeds) {
  1380. $avgspeed += $i;
  1381. }
  1382. $avgspeed /= scalar(@old_speeds);
  1383. if ($avgspeed == 0) {
  1384. return "-";
  1385. }
  1386. my $remainbytes = $totals{'total size'} - $totals{'total bytes'};
  1387. my $etasec = $remainbytes / $avgspeed;
  1388. $h = int($etasec/(60*60));
  1389. $m = int(($etasec-(60*60*$h))/60);
  1390. $s = $etasec-(60*60*$h)-(60*$m);
  1391. if($h > 240){ # likely bogus...just punt
  1392. return "-";
  1393. }
  1394. return sprintf("%.2d:%.2d:%.2d", $h, $m, $s);
  1395. }
  1396. }
  1397. #########################################################################################
  1398. # Checks the last paint time and updates the screen if necessary. Also checks for
  1399. # keyboard keys.
  1400. #########################################################################################
  1401. sub drawScreenAndHandleKeys {
  1402. $daemon and return; # don't draw screen when in daemon mode...RC keys handled elsewhere
  1403. if($showinghelpscreen){
  1404. cursorPos(40, 14);
  1405. pc("ETA: " . getETA(), 'bold green');
  1406. pc(")", 'bold white');
  1407. cursorPos(0, $hchar);
  1408. }
  1409. elsif((Time::HiRes::tv_interval(\@lastdrawtime) > 0.5) or # Refresh screen every 0.5sec max
  1410. (usingThreadedDecoding() and $decMsgQ->pending > 0)){ # or we got status messages from decoder thread
  1411. ($wchar, $hchar, $wpixels, $hpixels) = GetTerminalSize();
  1412. if($oldwchar != $wchar){
  1413. $oldwchar and statMsg("Terminal was resized (new width = $wchar), redrawing");
  1414. clearScreen();
  1415. drawBorder();
  1416. }
  1417. $oldwchar = $wchar;
  1418. @lastdrawtime = Time::HiRes::gettimeofday();
  1419. drawHeader();
  1420. drawConnInfos();
  1421. drawStatusMsgs();
  1422. cursorPos(0, $hchar);
  1423. pc("'?' for help> ", 'bold white');
  1424. }
  1425. my $char;
  1426. while (defined ($char = getch()) ) { # have a key
  1427. $char =~ s/[\r\n]//;
  1428. handleKey($char);
  1429. }
  1430. }
  1431. #########################################################################################
  1432. # Simple helper to determine if we're using threaded or nonthreaded decoding.
  1433. # It looks at the dthreadct variable and returns 1 if dthreadct > 0.
  1434. #########################################################################################
  1435. sub usingThreadedDecoding {
  1436. return ($dthreadct > 0);
  1437. }
  1438. #########################################################################################
  1439. # getch -- gets a key in nonblocking mode
  1440. #########################################################################################
  1441. sub getch {
  1442. # $daemon and return;
  1443. # ReadMode ('cbreak');
  1444. # my $char;
  1445. # $char = ReadKey(-1);
  1446. # ReadMode ('normal'); # restore normal tty settings
  1447. # return $char;
  1448. }
  1449. #########################################################################################
  1450. # Does bandwidth throttling
  1451. #########################################################################################
  1452. sub doThrottling {
  1453. not $targkBps and return; # Max setting, don't throttle.
  1454. $quitnow and return; # Don't bother if quitting
  1455. my $curbps = getCurrentSpeed(1)/1024; # in kBps
  1456. # TODO: Using percentages could likely make this way better.
  1457. # (ie. inc/dec sleep duration by error percentage %)
  1458. if($curbps > $targkBps){ # We're going too fast...
  1459. if($sleepdur == 0){
  1460. $sleepdur = 0.001; # arbitrary 1ms add
  1461. }
  1462. else{
  1463. $sleepdur *= 1.5;
  1464. }
  1465. if($sleepdur > 1.0){ # cap at 1 second sleep time, which is rediculously long anyway
  1466. $sleepdur = 1.0;
  1467. }
  1468. }
  1469. elsif($curbps < $targkBps){
  1470. if($sleepdur > 0){
  1471. if($sleepdur < 0.00001){ # lowest thresshold at 10us
  1472. $sleepdur = 0;
  1473. }
  1474. else{
  1475. $sleepdur -= ($sleepdur * 0.5);
  1476. }
  1477. }
  1478. }
  1479. if($sleepdur > 0){ # throttle if appropriate
  1480. select undef, undef, undef, $sleepdur;
  1481. }
  1482. }
  1483. #########################################################################################
  1484. # Trim the middle out of a string to shorten it to a target length
  1485. #########################################################################################
  1486. sub trimString {
  1487. my $string = shift;
  1488. my $target_len = shift;
  1489. my $len = length($string);
  1490. if($target_len >= $len || $target_len < 5) {
  1491. return $string;
  1492. }
  1493. my $chop = $len - $target_len + 3; # 3 for the ...
  1494. substr($string, ($len - $chop) / 2, $chop) = "...";
  1495. return $string;
  1496. }
  1497. #########################################################################################
  1498. # Handles a keypress
  1499. #########################################################################################
  1500. sub handleKey {
  1501. if($showinghelpscreen){
  1502. $showinghelpscreen = 0;
  1503. clearScreen();
  1504. $oldwchar = 0; # Hack to force border(s) to be redrawn
  1505. return; # cancel help screen display
  1506. }
  1507. my $key = shift;
  1508. if($key =~ /q/){
  1509. $quitnow = 1;
  1510. statMsg("User forced quit...exiting...");
  1511. # TODO: Close open files and delete parts files.
  1512. drawStatusMsgs();
  1513. updateBWStartPts();
  1514. }
  1515. elsif($key =~ /1/){
  1516. $targkBps = $lowbw;
  1517. statMsg("Setting bandwidth to low value ($lowbw" . "kBps)");
  1518. drawStatusMsgs();
  1519. updateBWStartPts();
  1520. }
  1521. elsif($key =~ /2/){
  1522. $targkBps = $medbw;
  1523. statMsg("Setting bandwidth to medium value ($medbw" . "kBps)");
  1524. drawStatusMsgs();
  1525. updateBWStartPts();
  1526. }
  1527. elsif($key =~ /3/){
  1528. $targkBps = 0; # set to high
  1529. statMsg("Setting bandwidth to maximum (unlimited)");
  1530. drawStatusMsgs();
  1531. updateBWStartPts();
  1532. }
  1533. elsif($key =~ /h/ or $key =~ /\?/){
  1534. statMsg("Displaying help screen at user's request");
  1535. showHelpScreen();
  1536. }
  1537. elsif($key =~ /c/){
  1538. $usecolor = $usecolor ^ 0x01; #invert value
  1539. }
  1540. elsif($key =~ /\+/){
  1541. if($targkBps){
  1542. $targkBps++;
  1543. statMsg("Nudging bandwidth setting up to " . $targkBps . "kBps");
  1544. drawStatusMsgs();
  1545. updateBWStartPts();
  1546. }
  1547. }
  1548. elsif($key =~ /-/){
  1549. if(!$targkBps){ # Set to unlimited
  1550. $targkBps = int(getCurrentSpeed(1)/1024)-1;
  1551. statMsg("Nudging bandwidth setting down to " . $targkBps . "kBps");
  1552. }
  1553. elsif($targkBps > 1){ # Bottom out at 1
  1554. $targkBps--;
  1555. statMsg("Nudging bandwidth setting down to " . $targkBps . "kBps");
  1556. }
  1557. drawStatusMsgs();
  1558. updateBWStartPts();
  1559. }
  1560. else {
  1561. statMsg("Unknown key: $key (try 'h' for help)");
  1562. }
  1563. }
  1564. #########################################################################################
  1565. # When the bandwidth changes, update all bw baselines for all connections
  1566. #########################################################################################
  1567. sub updateBWStartPts {
  1568. foreach my $i (1..$connct){
  1569. my $c = $conn[$i-1];
  1570. $c->{'bwstartbytes'} = $c->{'filebytes'};
  1571. @{$c->{'bwstarttime'}} = Time::HiRes::gettimeofday();
  1572. }
  1573. }
  1574. #########################################################################################
  1575. # Draws the header that contains summary info etc.
  1576. #########################################################################################
  1577. sub drawHeader(){
  1578. cursorPos(2, 1);
  1579. my $len = 0;
  1580. $len += pc("nzbperl v.$version", 'bold red');
  1581. $len += pc(" :: ", 'bold white');
  1582. $len += pc("noisybox.net", 'bold red');
  1583. my $queuedCount = countQueuedNZBFiles();
  1584. if($queuedCount > 0){
  1585. $len += pc(" [", 'bold blue');
  1586. $len += pc(sprintf("+%d nzb files queued", $queuedCount), 'bold cyan');
  1587. $len += pc("]", 'bold blue');
  1588. }
  1589. if(scalar @rc_clients > 0){
  1590. $len += pc(" [", 'bold blue');
  1591. $len += pc(sprintf("%d remotes", scalar @rc_clients), 'bold cyan');
  1592. $len += pc("]", 'bold blue');
  1593. }
  1594. pc((' ' x ($wchar-$len-4)), 'white');
  1595. cursorPos(2, 3);
  1596. $len += pc("Files remaining: ", 'bold white');
  1597. $len += pc($totals{'total file ct'} - $totals{'finished files'}, 'bold green');
  1598. $len += pc(" of ", 'white');
  1599. $len += pc($totals{'total file ct'}, 'bold green');
  1600. my $dlperc = $totals{'total size'} == 0 ? 0 : int(100.0*$totals{'total bytes'} / $totals{'total size'});
  1601. $len += pc(' [', 'bold blue');
  1602. $len += pc(hrsv($totals{'total bytes'}) . 'B', 'bold green');
  1603. $len += pc('/', 'bold white');
  1604. $len += pc(hrsv($totals{'total size'}) . 'B', 'bold green');
  1605. $len += pc(']', 'bold blue');
  1606. $len += pc(" ", 'white');
  1607. $len += pc($dlperc. '%', 'bold yellow');
  1608. $len += pc(" ETA: ", 'bold white');
  1609. $len += pc(getETA(), 'bold yellow');
  1610. pc((' ' x ($wchar-$len-4)), 'white');
  1611. cursorPos(2, 2);
  1612. $len = pc("Current speed: ", 'bold white');
  1613. $len += pc(getCurrentSpeed(), 'bold green');
  1614. $len += pc(" (", 'bold blue');
  1615. $len += pc("target", 'white');
  1616. $len += pc(' = ', 'white');
  1617. if($targkBps){
  1618. $len += pc(hrsv($targkBps*1024) . "Bps", 'bold green');
  1619. }
  1620. else{
  1621. $len += pc("unlimited!", 'bold red');
  1622. }
  1623. $len += pc(")", 'bold blue');
  1624. $len += pc(" Session speed: ", 'bold white');
  1625. $len += pc(getTotalSpeed(), 'bold green');
  1626. pc((' ' x ($wchar-$len-4)), 'white');
  1627. }
  1628. #########################################################################################
  1629. # Draws statuses for all individual connections
  1630. #########################################################################################
  1631. sub drawConnInfos(){
  1632. my $startrow = 6;
  1633. my $len;
  1634. foreach my $i(1..$connct){
  1635. my $conn = $conn[$i-1];
  1636. cursorPos(2, $startrow+(3*($i-1)));
  1637. if(not defined($conn->{'file'})){
  1638. if(scalar(@queuefileset) == 0){
  1639. # This connection has no more work to do...
  1640. $len = pc(sprintf("%d: Nothing left to do...", $i), 'bold cyan');
  1641. if(!defined($conn->{'sock'})){ # connection closed
  1642. $len += pc(" [", 'bold white');
  1643. $len += pc("closed", 'bold red');
  1644. $len += pc("]", 'bold white');
  1645. }
  1646. pc((' ' x ($wchar-$len-4)), 'white');
  1647. cursorPos(2, $startrow+(3*($i-1))+1);
  1648. $len = pc(defined($forever) ? " <waiting for more files to come in>" :
  1649. " <waiting for others to finish>", 'bold cyan');
  1650. pc((' ' x ($wchar-$len-4)), 'white');
  1651. }
  1652. elsif(defined($lastDiskFullTime)) { # connection waiting on free disk space
  1653. $len = pc(sprintf("%d: Waiting for free space on disk...[%d%% free, limit %d%%]", $i, $lastDiskFreePerc, $diskfree), 'bold yellow');
  1654. pc((' ' x ($wchar-$len-4)), 'white');
  1655. cursorPos(2, $startrow+(3*($i-1))+1);
  1656. $len = pc(sprintf(" <last check was %d%% free, limit is %d%%>", $lastDiskFreePerc, $diskfree), 'bold white');
  1657. pc((' ' x ($wchar-$len-4)), 'white');
  1658. }
  1659. next;
  1660. }
  1661. if(!defined($conn->{'sock'})){ # connection closed
  1662. $len = pc(sprintf("%d: ", $i), 'bold white');
  1663. $len += pc("Connection is closed", 'bold red');
  1664. if($conn->{'sleep start'}){ # will be a reconnect
  1665. my $remain = $reconndur - (time - $conn->{'sleep start'});
  1666. $len += pc(sprintf(" (reconnect in %s)", hrtv($remain)), 'bold yellow');
  1667. }
  1668. pc((' ' x ($wchar-$len-4)), 'white');
  1669. cursorPos(2, $startrow+(3*($i-1))+1);
  1670. pc((' ' x ($wchar-4)), 'white');
  1671. next;
  1672. }
  1673. my $file = $conn->{'file'};
  1674. my $filesize = $file->{'totalsize'};
  1675. my $filebytesread = $conn->{'filebytes'};
  1676. my $segnum = $conn->{'segnum'}+1;
  1677. my $segct = scalar @{$conn->{'file'}->{'segments'}};
  1678. my $segbytesread = $conn->{'segbytes'};
  1679. my $cursegsize = @{$file->{'segments'}}[$segnum-1]->{'size'};
  1680. $len = pc(sprintf("%d: Downloading: ", $i), 'bold white');
  1681. my $fn = $file->{'name'};
  1682. if( length($fn) + $len > $wchar-4){
  1683. $fn = substr($fn, 0, $wchar-4-$len);
  1684. }
  1685. $len += pc($fn, 'white');
  1686. if($len < $wchar-4){
  1687. pc(' ' x ($wchar-$len-4), 'white');
  1688. }
  1689. cursorPos(2, $startrow+(3*($i-1))+1);
  1690. my $perc = 0;
  1691. $filesize and $perc = int(($filebytesread/$filesize)*25);
  1692. if ($noansi) {
  1693. ($perc > 25) and $perc = 25; # cap progress bar length
  1694. $len = pc(" |", 'bold white');
  1695. if($perc){
  1696. $len += pc('#' x ($perc-1), 'bold white');
  1697. $len += pc('#', 'bold red');
  1698. }
  1699. $len += pc('-' x (25-$perc), 'white');
  1700. $len += pc("| ", 'bold white');
  1701. }
  1702. else {
  1703. $len = pc("\x1B(0" . " [", 'bold white');
  1704. if($perc){
  1705. $len += pc('a' x ($perc-1), 'bold white');
  1706. $len += pc('a', 'bold red');
  1707. }
  1708. $len += pc('q' x (25-$perc), 'white');
  1709. $len += pc("] " . "\x1B(B", 'bold white');
  1710. }
  1711. if($filesize){
  1712. $len += pc( sprintf("%2d", ($filebytesread/$filesize)*100) . "%", 'bold yellow');
  1713. }
  1714. else{
  1715. $len += pc("??%", 'bold yellow');
  1716. }
  1717. $len += pc(' ' x (7-length(hrsv($filebytesread))) . "[", 'bold white');
  1718. #$len += pc(sprintf("%5s", hrsv($filebytesread)), 'bold green');
  1719. $len += pc(hrsv($filebytesread), 'bold green');
  1720. $len += pc("/", 'bold white');
  1721. $len += pc(hrsv($filesize), 'bold green');
  1722. $len += pc("]", 'bold white');
  1723. $len += pc(" [part ", 'bold white');
  1724. $len += pc($segnum, 'bold cyan');
  1725. $len += pc("/", 'bold white');
  1726. $len += pc($segct, 'bold cyan');
  1727. $len += pc(" ", 'bold white');
  1728. $len += pc(sprintf("%4s", hrsv($segbytesread)), 'bold cyan');
  1729. $len += pc("/", 'bold white');
  1730. $len += pc(hrsv($cursegsize), 'bold cyan');
  1731. $len += pc("]", 'bold white');
  1732. pc((' ' x ($wchar - $len - 4)), 'white');
  1733. }
  1734. }
  1735. #########################################################################################
  1736. sub drawStatusMsgs {
  1737. # TODO: Consider saving state about status messages -- could save cycles by not
  1738. # automatically drawing every time.
  1739. $showinghelpscreen and return;
  1740. return unless defined($wchar); # to prevent decoder thread from trying to draw...
  1741. my $row = 3*$connct + 6 + 1;
  1742. my $statuslimit = $hchar - 9 - (3*$connct); # number of lines to show.
  1743. # Pull any decode messages from the queue and append them
  1744. # This might not be the *best* place for this...
  1745. while(usingThreadedDecoding() and $decMsgQ->pending > 0){
  1746. statMsg($decMsgQ->dequeue);
  1747. }
  1748. # Trim status messages to size
  1749. while( scalar(@statusmsgs) > $statuslimit){
  1750. shift @statusmsgs;
  1751. }
  1752. foreach my $line (@statusmsgs){
  1753. cursorPos(2, $row);
  1754. if(length($line) > ($wchar-4)){
  1755. $line = substr($line, 0, $wchar-4); # Clip line
  1756. }
  1757. else{
  1758. $line .= (' ' x ($wchar-4-length($line)));
  1759. }
  1760. pc($line, 'white');
  1761. $row++;
  1762. }
  1763. cursorPos(0, $hchar);
  1764. pc("'?' for help> ", 'bold white');
  1765. }
  1766. #########################################################################################
  1767. # Draws a border around the screen.
  1768. #########################################################################################
  1769. sub drawBorder {
  1770. drawVLine(0);
  1771. drawVLine($wchar);
  1772. drawHLine(0, "top");
  1773. drawHLine(4, "middle");
  1774. drawHLine(1+5+(3*$connct), "middle");
  1775. drawHLine($hchar-2, "bottom");
  1776. }
  1777. sub drawHLine {
  1778. my $ypos = shift;
  1779. my $hpos = shift;
  1780. cursorPos(0, $ypos);
  1781. if ($noansi) {
  1782. pc('+' . ('-' x ($wchar-2)) . '+', 'bold white');
  1783. }
  1784. else {
  1785. if ($hpos eq "top") {
  1786. pc("\x1B(0" . 'l' . ('q' x ($wchar-2)) . 'k' . "\x1B(B", 'bold white');
  1787. }
  1788. elsif ($hpos eq "middle") {
  1789. pc("\x1B(0" . 't' . ('q' x ($wchar-2)) . 'u' . "\x1B(B", 'bold white');
  1790. }
  1791. elsif ($hpos eq "bottom") {
  1792. pc("\x1B(0" . 'm' . ('q' x ($wchar-2)) . 'j' . "\x1B(B", 'bold white');
  1793. }
  1794. }
  1795. }
  1796. sub drawVLine {
  1797. my $xpos = shift;
  1798. my $height = shift;
  1799. not $height and $height = ($hchar-2);
  1800. foreach(0..$height){
  1801. cursorPos($xpos, $_);
  1802. if ($noansi) {
  1803. pc('|', 'bold white');
  1804. }
  1805. else {
  1806. pc("\x1B(0" . "x" . "\x1B(B", 'bold white');
  1807. }
  1808. }
  1809. }
  1810. #########################################################################################
  1811. # helper for printing in color (or not)
  1812. #########################################################################################
  1813. sub pc {
  1814. my $str = shift;
  1815. printMessage($str);
  1816. # my ($string, $colstr) = @_;
  1817. # not defined($colstr) and $colstr = "white"; # default to plain white
  1818. # $daemon and return length($string);
  1819. # if($usecolor){
  1820. # print colored ($string, $colstr);
  1821. # }
  1822. # else{
  1823. # print $string;
  1824. # }
  1825. # return length($string);
  1826. }
  1827. sub clearScreen {
  1828. # !$daemon and
  1829. # $terminal->Tputs('cl', 1, *STDOUT); # clears screen
  1830. }
  1831. #########################################################################################
  1832. # Positions the cursor at x,y. Looks at $daemon first.
  1833. #########################################################################################
  1834. sub cursorPos {
  1835. # my ($x, $y) = @_;
  1836. # !$daemon and
  1837. # $terminal->Tgoto('cm', $x, $y, *STDOUT);
  1838. }
  1839. #########################################################################################
  1840. # Print or log, depending on $silent
  1841. #########################################################################################
  1842. sub porl {
  1843. porlp(shift, $daemon);
  1844. }
  1845. #########################################################################################
  1846. # porlp :: "Print or log [with] param" -
  1847. #########################################################################################
  1848. sub porlp {
  1849. my $msg = shift;
  1850. my $lognotprint = shift;
  1851. if ($lognotprint){
  1852. chomp $msg;
  1853. statMsg($msg);
  1854. } else{
  1855. printMessage($msg);
  1856. }
  1857. }
  1858. #########################################################################################
  1859. # statOrQ - calls statMsg or enqueues the message, based on the value of $dthreadct,
  1860. # which governs if we're using a threaded approach or not.
  1861. #########################################################################################
  1862. sub statOrQ {
  1863. my $msg = shift;
  1864. if(usingThreadedDecoding()){
  1865. $decMsgQ->enqueue($msg);
  1866. }
  1867. else{
  1868. statMsg($msg);
  1869. }
  1870. }
  1871. #########################################################################################
  1872. # Adds a status message with timestamp
  1873. #########################################################################################
  1874. sub statMsg {
  1875. my $str = shift;
  1876. my @t = localtime;
  1877. my $msg = sprintf("%0.2d:%0.2d - %s", $t[2], $t[1], $str);
  1878. push @statusmsgs, $msg;
  1879. printMessage($str."\n");
  1880. return 1;
  1881. }
  1882. #########################################################################################
  1883. # Socket send that can handle both SSL and regular socket...
  1884. #########################################################################################
  1885. sub sockSend {
  1886. my ($sock, $msg) = @_;
  1887. if (ref($sock) eq "IO::Socket::SSL") {
  1888. $sock->syswrite($msg, undef);
  1889. }
  1890. else {
  1891. send $sock, $msg, 0;
  1892. }
  1893. }
  1894. #########################################################################################
  1895. # Reads a line from the socket in a blocking manner.
  1896. #########################################################################################
  1897. sub blockReadLine {
  1898. my $sock = shift;
  1899. my ($line, $buff) = ('', '');
  1900. while(1){
  1901. if (ref($sock) eq "IO::Socket::SSL") {
  1902. $sock->sysread($buff, 1024);
  1903. }
  1904. else{
  1905. defined(recv($sock, $buff, 1024, 0)) or last;
  1906. }
  1907. $line .= $buff;
  1908. last if $line =~ /\r\n$/;
  1909. }
  1910. return $line;
  1911. }
  1912. #########################################################################################
  1913. # Gracefully close down all server connections.
  1914. #########################################################################################
  1915. sub disconnectAll {
  1916. foreach my $i (1..$connct){
  1917. my $sock = $conn[$i-1]->{'sock'},
  1918. printMessage("Closing down connection #$i...\n");
  1919. not $sock and printMessage("(already closed)\n") and next;
  1920. sockSend($sock, "QUIT\r\n");
  1921. my $line = blockReadLine($sock);
  1922. $line =~ /^205/ and printMessage("closed gracefully!");
  1923. print "\n";
  1924. if (ref($sock) eq "IO::Socket::SSL") {
  1925. $sock->shutdown( 2 );
  1926. $sock->close( SSL_no_shutdown => 1);
  1927. }
  1928. else {
  1929. close($sock);
  1930. }
  1931. $conn[$i-1]->{'sock'} = undef;
  1932. }
  1933. }
  1934. #########################################################################################
  1935. # human readable time value (from seconds)
  1936. #########################################################################################
  1937. sub hrtv {
  1938. my $sec = shift;
  1939. if($sec < 60){
  1940. return $sec . "s";
  1941. }
  1942. my $h = int($sec/(60*60));
  1943. my $m = int(($sec - ($h*60*60))/60.0);
  1944. my $s = $sec - ($h*60*60) - ($m*60);
  1945. if($h){
  1946. return sprintf("%02d:%02d:%02d", $h, $m, $s);
  1947. }
  1948. else{
  1949. return sprintf("%02d:%02d", $m, $s);
  1950. }
  1951. }
  1952. #########################################################################################
  1953. # human readable size value
  1954. #########################################################################################
  1955. sub hrsv {
  1956. my $size = shift; # presumed bytes
  1957. $size = 0 unless defined($size);
  1958. my $k = 1.0*$size/1024;
  1959. my $m = 1.0*$size/(1024*1024);
  1960. my $g = 1.0*$size/(1024*1024*1024);
  1961. if($g > 1){
  1962. return sprintf("%0.2fG", $g);
  1963. }
  1964. if($m > 1){
  1965. return sprintf("%0.2fM", $m);
  1966. }
  1967. if($k > 1){
  1968. return sprintf("%dk", $k);
  1969. }
  1970. return sprintf("%0.2f", $size);
  1971. }
  1972. #########################################################################################
  1973. # read password without echoing it
  1974. #########################################################################################
  1975. sub readPassword {
  1976. # ReadMode 2; # no echo
  1977. # my $pw = <STDIN>;
  1978. # chomp $pw;
  1979. # ReadMode 0; # default
  1980. # print "\n";
  1981. # return $pw;
  1982. }
  1983. #########################################################################################
  1984. # Determines the total file size for all segments in the NZB file
  1985. #########################################################################################
  1986. sub computeTotalNZBSize {
  1987. my @fileset = @_;
  1988. my $tot = 0;
  1989. foreach my $file (@fileset){
  1990. foreach my $seg (@{$file->{'segments'}}){
  1991. $tot += $seg->{'size'};
  1992. }
  1993. }
  1994. return $tot;
  1995. }
  1996. #########################################################################################
  1997. # Parse NZB file and return array of files
  1998. # TODO: The structure returned from this function should really be documented....but
  1999. # for now, if you need it, use Dumper to view the format. Should be self explanatory.
  2000. #########################################################################################
  2001. sub parseNZB {
  2002. my $nzbfilename = shift;
  2003. my $lognoprint = shift;
  2004. $nzbfiles{'files'}->{basename($nzbfilename)}->{'read'} = 1; # set flag indicating we've processed it
  2005. my $nzbdir = derivePath($nzbfilename);
  2006. if ((defined($dlpath)) and (defined($dlcreate))) {
  2007. my $nzbbase = basename($nzbfilename);
  2008. if ($nzbbase =~ /msgid_[0-9]*_(.*).nzb/) {
  2009. # Filter name from NewzBin style names
  2010. $nzbdir = $1 . "/";
  2011. }
  2012. elsif ($nzbbase =~ /(.*).nzb/) {
  2013. # Strip the .nzb extension and
  2014. $nzbdir = $1 . "/";
  2015. }
  2016. else {
  2017. # Just use the nzb file as a directory itself
  2018. $nzbdir = $nzbbase . "/";
  2019. }
  2020. }
  2021. $nzbdir .= '/' unless $nzbdir =~ /\/$/;
  2022. my $parser = new XML::DOM::Parser;
  2023. my @fileset;
  2024. porlp("Loading and parsing nzb file: " . $nzbfilename . "\n", $lognoprint);
  2025. my $nzbdoc;
  2026. eval {
  2027. $nzbdoc = $parser->parsefile($nzbfilename);
  2028. };
  2029. if($@){
  2030. my $errmsg = trimWS($@);
  2031. if($lognoprint){
  2032. statMsg("The nzb file is BROKEN and the XML could not be parsed.");
  2033. }
  2034. else{
  2035. pc("\n");
  2036. pc(" Sorry, but nzb file is broken! The xml could not be parsed:\n", 'bold yellow');
  2037. pc("\n");
  2038. pc(" $errmsg\n\n", 'bold yellow');
  2039. pc(" *** nzbperl requires valid, well-formed XML documents.\n\n", 'bold red');
  2040. }
  2041. return undef;
  2042. }
  2043. my $files = $nzbdoc->getElementsByTagName("file");
  2044. my $totalsegct = 0;
  2045. foreach my $i (0..$files->getLength()-1){
  2046. my $fileNode = $files->item($i);
  2047. my $subj = $fileNode->getAttributes()->getNamedItem('subject');
  2048. my $postdate = $fileNode->getAttributes()->getNamedItem('date');
  2049. my %file;
  2050. $file{'nzb path'} = $nzbdir;
  2051. $file{'nzb file'} = basename($nzbfilename);
  2052. $file{'name'} = $subj->getValue();
  2053. $file{'date'} = $postdate->getValue();
  2054. my @groupnames;
  2055. for my $group ($fileNode->getElementsByTagName('group')) {
  2056. push @groupnames, $group->getFirstChild()->getNodeValue();
  2057. }
  2058. $file{'groups'} = \@groupnames;
  2059. my @segments;
  2060. for my $seg ($fileNode->getElementsByTagName('segment')) {
  2061. my %seghash;
  2062. my $size = $seg->getAttributes()->getNamedItem('bytes')->getValue();
  2063. $file{'totalsize'} += $size;
  2064. my $segNumber = $seg->getAttributes()->getNamedItem('number')->getValue();
  2065. $seghash{'msgid'} = $seg->getFirstChild()->getNodeValue();
  2066. $seghash{'size'} = $size;
  2067. $seghash{'number'} = $segNumber;
  2068. push @segments, \%seghash;
  2069. }
  2070. # If segment numbers are present, use them to sort.
  2071. if (defined($segments[0]) && defined($segments[0]->{'number'})){
  2072. @segments = sort {
  2073. $a->{'number'} <=> $b->{'number'} } @segments;
  2074. }
  2075. $totalsegct += scalar @segments;
  2076. $file{'segments'} = \@segments;
  2077. push @fileset, \%file;
  2078. }
  2079. $nzbdoc->dispose;
  2080. porlp("Loaded $totalsegct total segments for " . $files->getLength() . " file(s).\n", $lognoprint);
  2081. @fileset = sortFilesBySubject($lognoprint, @fileset); # It checks $sort inside
  2082. @fileset = resetLastOnNzbFlag(@fileset);
  2083. return @fileset;
  2084. }
  2085. #########################################################################################
  2086. # Filters out files if there is a filter regex, and skips over files from --skip <n>
  2087. #########################################################################################
  2088. sub regexAndSkipping {
  2089. my @fileset = @_;
  2090. if(defined($filterregex)){ # the inclusive filter
  2091. @fileset = filterFilesOnSubject(1, $filterregex, @fileset);
  2092. }
  2093. if(defined($ifilterregex)){ # the exclusive (inverse) filter
  2094. @fileset = filterFilesOnSubject(0, $ifilterregex, @fileset);
  2095. }
  2096. if($skipfilect){
  2097. if($skipfilect >= scalar @fileset){
  2098. pc("\nWhoops: --skip $skipfilect would skip ALL " . scalar @fileset .
  2099. " files...aborting!\n\n", 'bold yellow') and shutdownClient();
  2100. }
  2101. printMessage("Removing $skipfilect files from nzb set (--skip $skipfilect)\n");
  2102. while($skipfilect > 0){
  2103. shift @fileset;
  2104. $skipfilect--;
  2105. }
  2106. }
  2107. @fileset = resetLastOnNzbFlag(@fileset);
  2108. return @fileset;
  2109. }
  2110. #########################################################################################
  2111. # Takes in a list of files and filters them based on subject.
  2112. #########################################################################################
  2113. sub filterFilesOnSubject {
  2114. my $inclusiveRegex = shift;
  2115. my $regex = shift;
  2116. my @fileset = @_;
  2117. printMessage("Filtering files on " . ($inclusiveRegex ? '' : 'inverse ') . "regular expression...\n");
  2118. my $orgsize = scalar @fileset;
  2119. my @nset;
  2120. while(scalar(@fileset) > 0){
  2121. my $f = shift @fileset;
  2122. if( ($inclusiveRegex and ($f->{'name'} =~ /$regex/)) or
  2123. ( not $inclusiveRegex and (not $f->{'name'} =~ /$regex/))){
  2124. push @nset, $f;
  2125. }
  2126. }
  2127. if(scalar @nset < 1){
  2128. pc("\nWhoops: Filter removed all files (nothing left)...aborting!\n\n", 'bold yellow') and shutdownClient();
  2129. }
  2130. printMessage(sprintf("Kept %d of %d files (filtered %d)\n", scalar(@nset), $orgsize, $orgsize-scalar(@nset)));
  2131. return @nset;
  2132. }
  2133. #########################################################################################
  2134. # Sorts files in a fileset based on the name
  2135. #########################################################################################
  2136. sub sortFilesBySubject {
  2137. my $quiet = shift;
  2138. my @fileset = @_;
  2139. if(!$nosort){
  2140. porlp("Sorting files by filename (subject)...", $quiet);
  2141. @fileset =
  2142. sort {
  2143. $a->{'name'} cmp $b->{'name'};
  2144. } @fileset;
  2145. porlp("finished.\n", $quiet);
  2146. }
  2147. return @fileset;
  2148. }
  2149. #########################################################################################
  2150. # Traverses a fileset and resets the islastonnzb flag.
  2151. #########################################################################################
  2152. sub resetLastOnNzbFlag {
  2153. my @fileset = @_;
  2154. foreach my $i (0..scalar(@fileset)-1){
  2155. if($i == scalar(@fileset)-1){
  2156. $fileset[$i]->{'lastonnzb'} = 1;
  2157. }
  2158. else{
  2159. $fileset[$i]->{'lastonnzb'} = 0;
  2160. }
  2161. }
  2162. return @fileset;
  2163. }
  2164. #########################################################################################
  2165. # Derives a path from a filename (passed on commandline).
  2166. # The result isn't necessarily absolute, can be relative
  2167. #########################################################################################
  2168. sub derivePath {
  2169. my $filename = shift;
  2170. if($filename =~ /\//){ # then it has path information, likely not windows compat
  2171. $filename =~ s/(^.*\/).*/$1/;
  2172. return $filename;
  2173. }
  2174. return cwd;
  2175. }
  2176. #########################################################################################
  2177. # Main entry point for NZB file sanity checking
  2178. #########################################################################################
  2179. sub doNZBSanityChecks(){
  2180. printMessage("Analyzing sanity of NZB file segment completeness...\n");
  2181. @suspectFileInd = getSuspectFileIndexes();
  2182. my $badfilect = scalar @suspectFileInd;
  2183. not $badfilect and pc("All files pass segment size sanity checks! Swell.\n", 'bold green') and return;
  2184. SMENUDONE:
  2185. while(1){
  2186. pc(sprintf("There are %d of %d files that may have missing or broken segments.\n", $badfilect, scalar @fileset), 'bold yellow');
  2187. pc("It is likely that these files will be unusable if downloaded.\n", 'bold yellow');
  2188. ($dropbad or $insane) and return; # User selection not needed.
  2189. print "\n How do you want to proceed?\n\n";
  2190. print " k)eep everything and try all files anyway (--insane)\n";
  2191. print " d)rop files suspected broken (--dropbad)\n";
  2192. print " v)iew gory details about broken segments\n";
  2193. print " q)uit now\n";
  2194. print "\n -> ";
  2195. while(1){
  2196. my $char;
  2197. if(defined ($char = getch()) ) { # have a key
  2198. print "\n";
  2199. if($char =~ /q/){
  2200. # shutdown
  2201. shutdownClient();
  2202. }
  2203. elsif($char =~ /k/){
  2204. print "Setting --insane option...\n";
  2205. $insane = 1;
  2206. last SMENUDONE;
  2207. }
  2208. elsif($char =~ /d/){
  2209. print "Setting --dropbad option...\n";
  2210. $dropbad = 1;
  2211. last SMENUDONE;
  2212. }
  2213. elsif($char =~ /v/){
  2214. showSuspectDetails(@suspectFileInd);
  2215. }
  2216. last;
  2217. }
  2218. else{
  2219. select undef, undef, undef, 0.1;
  2220. }
  2221. }
  2222. }
  2223. }
  2224. #########################################################################################
  2225. # Shows details about suspect files...
  2226. #########################################################################################
  2227. sub showSuspectDetails {
  2228. my @susFileInd = @_;
  2229. foreach my $fileind (1..scalar @susFileInd){
  2230. my $file = @fileset[$susFileInd[$fileind-1]];
  2231. my $avgsize = avgFilesize($file);
  2232. print "------------------------------------------------------\n";
  2233. printf(" * File: %s\n", $file->{'name'});
  2234. printf(" Posted on: %s (%d days ago)\n",
  2235. scalar localtime $file->{'date'},
  2236. (time - $file->{'date'})/(60*60*24) );
  2237. printf(" Adjusted average part size = %d bytes\n", $avgsize);
  2238. my @sids = getSuspectSegmentIndexes($file, $avgsize);
  2239. foreach my $si (@sids){
  2240. my $seg = @{$file->{'segments'}}[$si];
  2241. my $percdiff = 100;
  2242. $avgsize and $percdiff = 100*(abs($seg->{'size'} - $avgsize)/$avgsize);
  2243. printf(" Part %d : %d bytes (%.2f%% error from average)\n",
  2244. $si+1, $seg->{'size'}, $percdiff);
  2245. }
  2246. }
  2247. print "------------------------------------------------------\n";
  2248. }
  2249. #########################################################################################
  2250. # Looks at the fileset and returns an array of file indexes that are suspect
  2251. #########################################################################################
  2252. sub getSuspectFileIndexes {
  2253. my @ret;
  2254. foreach my $fileind (1..scalar @fileset){
  2255. my $file = $fileset[$fileind-1];
  2256. my $avg = avgFilesize($file);
  2257. #printf("File has average size = %d\n", $avg);
  2258. my $segoffct = 0;
  2259. my @suspectSegInd = getSuspectSegmentIndexes($file, $avg);
  2260. if(scalar @suspectSegInd){
  2261. push @ret, $fileind-1;
  2262. }
  2263. }
  2264. return @ret;
  2265. }
  2266. #########################################################################################
  2267. sub getSuspectSegmentIndexes {
  2268. my $MAX_OFF_PERC = 25; # Percentage of segment size error/diff to trigger invalid
  2269. my ($file, $avg) = @_;
  2270. my @ret;
  2271. foreach my $i (1..(scalar @{$file->{'segments'}}-1)){ # Last segment is allowed to slide...
  2272. my $seg = @{$file->{'segments'}}[$i-1];
  2273. my $percdiff = 100;
  2274. $avg and $percdiff = 100*(abs($seg->{'size'} - $avg)/$avg);
  2275. #printf(" seg $i of %d is %0.2f off avg [%d versus %d (avg)]\n", scalar @{$file->{'segments'}}, $percdiff, $seg->{'size'}, $avg);
  2276. if($percdiff > $MAX_OFF_PERC){
  2277. push @ret, $i-1;
  2278. }
  2279. }
  2280. return @ret;
  2281. }
  2282. #########################################################################################
  2283. sub dropSuspectFiles(){ my @newset; my $dropct = 0; foreach my $i (0..scalar @fileset-1){
  2284. if ((defined($suspectFileInd[0])) && ($i == $suspectFileInd[0])) {
  2285. my $ind = shift @suspectFileInd;
  2286. my $file = $fileset[$ind];
  2287. printMessage(sprintf("Dropping [%s] from filset (suspect)\n", $file->{'name'}));
  2288. $dropct++;
  2289. next;
  2290. }
  2291. push @newset, shift @fileset;
  2292. }
  2293. @fileset = @newset;
  2294. pc(sprintf("Dropped %d suspect files from NZB (%d files remain)\n", $dropct, scalar @fileset), 'bold yellow');
  2295. printMessage(" -> short delay (for user review)\n");
  2296. foreach(5,4,3,2,1){
  2297. sleep 1;
  2298. }
  2299. printMessage("...let's go!\n");
  2300. }
  2301. #########################################################################################
  2302. # Not a true average, but an average of all segments except the last one...
  2303. # ...unless there's only one segment, in which case it's the segment size.
  2304. #########################################################################################
  2305. sub avgFilesize {
  2306. my $file = shift;
  2307. my @segs = @{$file->{'segments'}};
  2308. return $segs[0]->{'size'} unless scalar @segs > 1;
  2309. my ($sum, $ct) = (0, 0);
  2310. foreach my $i (1..scalar(@segs)){
  2311. my $seg = $segs[$i-1];
  2312. last unless $i < scalar(@segs);
  2313. $ct++;
  2314. $sum += $seg->{'size'};
  2315. }
  2316. return $sum*1.0/($ct*1.0);
  2317. }
  2318. #########################################################################################
  2319. # Parse command line options and assign sane globals etc.
  2320. #########################################################################################
  2321. sub handleCommandLineOptions {
  2322. my @saveargs = @ARGV;
  2323. # This extra call is required to set up the --config option, expected below
  2324. GetOptions(%optionsmap);
  2325. my $errmsg;
  2326. # This is the facility for trapping stderr from GetOptions, so that we
  2327. # can pretty print it at the bottom of the help screen.
  2328. local $SIG{'__WARN__'} = sub {
  2329. $errmsg = $_[0];
  2330. chomp $errmsg;
  2331. };
  2332. # First see if the config file is there, if so, slurp options from it.
  2333. my $optionsAreOk;
  2334. if(-e $configfile){
  2335. readConfigFileOptions();
  2336. $optionsAreOk = eval 'GetOptions(%optionsmap)';
  2337. return $errmsg unless $optionsAreOk;
  2338. } else {
  2339. printMessage("Config file $configfile does not exist. Skipping.\n");
  2340. }
  2341. # Now restore the commandline args and parse those (overriding config file options)
  2342. @ARGV = @saveargs; # restore
  2343. $optionsAreOk = eval 'GetOptions(%optionsmap)';
  2344. return $errmsg unless $optionsAreOk;
  2345. if($help){
  2346. return "";
  2347. }
  2348. $nocolor and $usecolor = 0;
  2349. not $optionsAreOk and return "";
  2350. if(usingThreadedDecoding()){
  2351. eval "
  2352. use threads;
  2353. use Thread::Queue;";
  2354. ($@) and return "ERROR: Could not use Perl thread modules.\r\n" .
  2355. " Try setting --dthreadct 0 to run with a single threaded Perl.";
  2356. }
  2357. if($recv_chunksize =~ /kb?$/i){
  2358. $recv_chunksize =~ s/kb?$//i;
  2359. $recv_chunksize = $recv_chunksize*1024;
  2360. }
  2361. if(defined($queuedir) and (not $queuedir =~ /^\//)){
  2362. return "--queuedir must specify an ABSOLUTE (not relative) path.";
  2363. }
  2364. if(not $ARGV[0] and (not defined $queuedir)){ # No NZB file given?
  2365. return "Missing nzb file or directory queue.";
  2366. }
  2367. if($server =~ /:\d+$/){
  2368. $port = $server;
  2369. $port =~ s/.*://;
  2370. $server =~ s/:.*//;
  2371. }
  2372. if(not length($server)){
  2373. $server = $ENV{'NNTPSERVER'};
  2374. not $server and return "Must provide --server or set \$NNTPSERVER environment";
  2375. }
  2376. $server = trimWS($server);
  2377. $dlpath = cwd unless (defined($dlpath) or defined($dlrelative));
  2378. if($dlpath and not $dlpath =~ /^\//){
  2379. return "--dlpath must specify an ABSOLUTE (not relative) path.";
  2380. }
  2381. # Make sure that dlpath ends with a slash
  2382. if($dlpath and (not ($dlpath =~ /\/$/))){
  2383. $dlpath .= '/';
  2384. ($dlpath =~ m#^([\w\d\s\.\_\-\/\\]+)$#) and $dlpath = $1; # untaint dlpath
  2385. }
  2386. if($dropbad and $insane){ # conflicting
  2387. return "Error: --dropbad and --insane are conflicting (choose one)";
  2388. }
  2389. if($forever and not (defined($rcport) or defined($queuedir))){
  2390. return "Error: --forever requires either --queuedir or --rcport.\n" .
  2391. " Please choose one and try again.";
  2392. }
  2393. if(defined($queuedir) and !$dropbad and !$insane){
  2394. return "Use of --queuedir requires either --dropbad or --insane.\n" .
  2395. " Please choose one and try again.";
  2396. }
  2397. if(defined($postDecProg) and not -e $postDecProg){
  2398. return "--postdec program \"$postDecProg\" does not exist.\n" .
  2399. " Please confirm the program and try again.";
  2400. }
  2401. if($dlpath and $dlrelative){ # conflicting options
  2402. return "Error: --dlrelative and --dlpath <dir> are conflicting (choose one)";
  2403. }
  2404. # Verify that output dir is writable...
  2405. if(defined($dlpath) and not -w $dlpath) {
  2406. return "Error: dlpath '$dlpath' is not writable!\n" .
  2407. " Please change the permissions or use a different directory.";
  2408. }
  2409. if(defined($DECODE_DBG_FILE)){
  2410. if(open(DBGTMP,">$DECODE_DBG_FILE")){
  2411. close DBGTMP; #all good
  2412. }
  2413. else{
  2414. return "The decode log file '$DECODE_DBG_FILE' is unwritable!";
  2415. }
  2416. }
  2417. if($port == -1) {
  2418. if (defined($ssl)) {
  2419. (undef, undef, $port, undef) = getservbyname("nntps", "tcp");
  2420. }
  2421. else {
  2422. (undef, undef, $port, undef) = getservbyname("nntp", "tcp");
  2423. }
  2424. }
  2425. if(defined($socks_server) and defined($http_proxy_server)){
  2426. return "Error: --socks_server and --http_proxy are conflicting (choose one)";
  2427. }
  2428. if(defined($dlcreate) and defined($dlcreategrp)){
  2429. return "Error: --dlcreate and --dlcreategrp are conflicting (choose one)";
  2430. }
  2431. if (defined($ssl)) {
  2432. eval "use IO::Socket::SSL;"; # use module only if option is enabled.
  2433. ($@) and return "ERROR: --ssl was specified, but IO::Socket::SSL isn't available.\r\n" .
  2434. " Please install IO::Socket::SSL to use --ssl and try again.";
  2435. }
  2436. if (defined($socks_server)) {
  2437. eval "use IO::Socket::Socks;"; # use module only if option enabled
  2438. ($@) and return "ERROR: --socks_server was specified, but IO::Socket::Socks isn't available.\r\n" .
  2439. " Please install IO::Socket::Socks to use a SOCKS server and try again.";
  2440. if ($socks_port == -1) {
  2441. if($socks_server =~ /:\d+$/){
  2442. $socks_port = $socks_server;
  2443. $socks_port =~ s/.*://;
  2444. $socks_server =~ s/:.*//;
  2445. }
  2446. else {
  2447. (undef, undef, $socks_port, undef) = getservbyname("socks", "tcp");
  2448. }
  2449. }
  2450. $socks_server = trimWS($socks_server);
  2451. }
  2452. if (defined($http_proxy_server)) {
  2453. eval "use Net::HTTPTunnel;"; # use module only if option enabled
  2454. ($@) and return "ERROR: --http_proxy was specified, but Net::HTTPTunnel isn't available.\r\n" .
  2455. " Please install Net::HTTPTunnel to use an HTTP proxy and try again.";
  2456. if($http_proxy_server =~ /:\d+$/){
  2457. $http_proxy_port = $http_proxy_server;
  2458. $http_proxy_port =~ s/.*://;
  2459. $http_proxy_server =~ s/:.*//;
  2460. }
  2461. else {
  2462. (undef, undef, $http_proxy_port, undef) = getservbyname("webcache", "tcp");
  2463. }
  2464. $http_proxy_server = trimWS($http_proxy_server);
  2465. }
  2466. if(defined($ipv6)){
  2467. eval "use IO::Socket::INET6;"; # use ipv6 module if option given
  2468. ($@) and return "ERROR: --ipv6 was given and the IO::Socket::INET6 module could not be found.\r\n" .
  2469. " You must install the IO::Socket::INET6 module to use IPv6";
  2470. }
  2471. # check tf-arg
  2472. if (!$tfuser) {
  2473. return "no tfuser given\n";
  2474. }
  2475. return undef; # success
  2476. }
  2477. #########################################################################################
  2478. # Helper to detect that uudeview is installed. Always a good idea, ya'know, since we're
  2479. # dependant on it!
  2480. #########################################################################################
  2481. sub haveUUDeview {
  2482. if(defined($uudeview)){ # Given on commandline or config file
  2483. if (-e $uudeview){
  2484. return 1;
  2485. }
  2486. printError("Warning: uudeview not found at location $uudeview\n");
  2487. }
  2488. my @paths = split /:/, $ENV{'PATH'}; # path sep different on winderz?
  2489. foreach my $p (@paths){
  2490. $p =~ s/\/$//;
  2491. $p = $p . "/uudeview";
  2492. if(-e $p){
  2493. printMessage("uudeview found: $p\n");
  2494. $uudeview = $p;
  2495. return 1;
  2496. }
  2497. }
  2498. printError("Error: uudeview not found in path...aborting!\n");
  2499. return 0;
  2500. }
  2501. #########################################################################################
  2502. # Reads options from the config file and tucks them into @ARGV, so that they all
  2503. # look like they were passd on the commandline. So, when this returns (successfully),
  2504. # ARGV contains the config file contents. ARGV must be preserved externally.
  2505. #########################################################################################
  2506. sub readConfigFileOptions(){
  2507. printMessage("Reading config options from $configfile...\n");
  2508. open CFG, "<$configfile" or die "Error opening $configfile for config options";
  2509. my $line;
  2510. my @opts;
  2511. while($line = <CFG>){
  2512. chomp $line;
  2513. $line =~ s/^\s+//;
  2514. $line =~ s/^-+//; # In case dashes in config file
  2515. $line =~ s/(\s+)?=(\s+)?/=/; # Remove whitespace around equals sign
  2516. next if $line =~ /^#/;
  2517. next unless length($line);
  2518. push @opts, "--$line";
  2519. }
  2520. close CFG;
  2521. @ARGV = @opts;
  2522. }
  2523. #########################################################################################
  2524. # Trim ws on both sides of string. Undef is ok.
  2525. #########################################################################################
  2526. sub trimWS {
  2527. my $s = shift;
  2528. return $s unless defined $s;
  2529. $s =~ s/^\s+//;
  2530. $s =~ s/\s+$//;
  2531. return $s;
  2532. }
  2533. #########################################################################################
  2534. # Checks for a newer version, disabled with --noupdate
  2535. #########################################################################################
  2536. sub checkForNewVersion {
  2537. $noupdate and return; # they don't want update checking
  2538. printMessage("Checking for availability of newer version...\n");
  2539. eval "use LWP::Simple;";
  2540. if($@){
  2541. printMessage("LWP::Simple is not installed, skipping up-to-date check.\n");
  2542. return;
  2543. }
  2544. my $remote_ver = eval "get \"$UPDATE_URL\"";
  2545. if(!defined($remote_ver)){
  2546. pc("Error fetching current version during update check: $!\n", 'bold red');
  2547. pc("Skipping up-to-date check.\n", 'bold yellow');
  2548. return;
  2549. }
  2550. chomp $remote_ver;
  2551. if($remote_ver eq $version){
  2552. printMessage("Look like you're running the most current version. Good.\n");
  2553. }
  2554. else{
  2555. pc("A newer version of nzbperl is available: ", 'bold red');
  2556. pc('version ' . $remote_ver . "\n", 'bold white');
  2557. pc("You should consider downloading it from ", 'bold white');
  2558. pc("http://noisybox.net/computers/nzbperl/\n", 'bold yellow');
  2559. pc("This delay is intentional: ");
  2560. foreach(1..8){
  2561. print "..." . (9-$_);
  2562. sleep 1;
  2563. }
  2564. pc("\n");
  2565. }
  2566. }
  2567. #########################################################################################
  2568. sub displayShortGPL {
  2569. print <<EOL
  2570. nzbperl version $version, Copyright (C) 2004 Jason Plumb
  2571. nzbperl comes with ABSOLUTELY NO WARRANTY; This is free software, and
  2572. you are welcome to redistribute it under certain conditions; Please
  2573. see the source for additional details.
  2574. EOL
  2575. ;
  2576. }
  2577. #########################################################################################
  2578. # Shows a help screen for interactive keys
  2579. #########################################################################################
  2580. sub showHelpScreen {
  2581. clearScreen();
  2582. print <<EOL
  2583. Hi. This is the nzbperl help screen.
  2584. You can use the following keys while we're running:
  2585. '1' : Switch to low bandwidth mode ($lowbw kBps)
  2586. '2' : Switch to med bandwidth mode ($medbw kBps)
  2587. '3' : Switch to high bandwidth mode (unlimited)
  2588. '+' : Nudge target bandwidth setting up 1 kBps
  2589. '-' : Nudge target bandwidth setting down 1 kBps
  2590. 'c' : Toggle color on or off
  2591. 'q' : Quit the program (aborts all downloads)
  2592. '?' : Show this help screen
  2593. Connected to $server:$port
  2594. (Your download is still in progress:
  2595. [ Press any key to return to the main screen ]
  2596. EOL
  2597. ;
  2598. drawVLine(0, 17);
  2599. drawVLine($wchar, 17);
  2600. drawHLine(0, 'top');
  2601. drawHLine(17, 'bottom');
  2602. cursorPos(40, 14);
  2603. pc("ETA: " . getETA(), 'bold green');
  2604. pc(")", 'bold white');
  2605. $showinghelpscreen = 1;
  2606. }
  2607. #########################################################################################
  2608. # Show program usage
  2609. #########################################################################################
  2610. sub showUsage {
  2611. my $errmsg = shift;
  2612. print <<EOL
  2613. nzbperl version $version -- usage:
  2614. nzbperl <options> <file1.nzb> ... <file.nzb>
  2615. where <options> are:
  2616. --config <file> : Use <file> for config options (default is ~/.nzbperlrc)
  2617. --server <server> : Usenet server to use (defaults to NNTPSERVER env var)
  2618. : Port can also be specified with --server <server:port>
  2619. --user <user> : Username for server (blank of not needed)
  2620. --pw <pass> : Password for server (blank to prompt if --user given)
  2621. --conn <n> : Use <n> server connections (default = 2)
  2622. --ssl : Connect to server using SSL (secure sockets layer).
  2623. : May be combined with --http_proxy or --socks_server to
  2624. : use a proxy server with SSL.
  2625. --socks_server <s>: Connect using <s> as a socks proxy server. Defaults to
  2626. : port 1080, but can use --socks_server <server:port> to
  2627. : use an alternative port.
  2628. --http_proxy <s> : Use <s> as an http proxy server to use. Defaults
  2629. : to port 8080, but can use --http_proxy <server:port> to
  2630. : use an alternative port.
  2631. --proxy_user <u> : Authenticate to the proxy using <u> as the username
  2632. --proxy_passwd <p>: Use <p> as the proxy user password (otherwise prompted)
  2633. --ipv6 : Use IPv6 sockets for communication
  2634. --keepparts : Keep all encoded parts files on disk after decoding
  2635. --keepbroken : Continue downloading files with broken/missing segments
  2636. : and leave the parts files on disk still encoded.
  2637. --keepbrokenbin : Decode and keep broken decoded files (binaries) on disk.
  2638. --dlpath <dir> : Download and decode all files to <dir>
  2639. : (default downloads to current dirctory)
  2640. --dlrelative : Download and decode to the dir that the nzbfiles are in
  2641. : (default downloads to current directory)
  2642. --dlcreate : Create download directories per nzb file
  2643. --dlcreategrp : Create download dirctories with usenet group names
  2644. --queuedir <dir> : Monitor <dir> for nzb files and queue new ones
  2645. --forever : Run forever, waiting for new nzbs (requires --queuedir)
  2646. --postdec <prog> : Run <prog> after each file is decoded, env var params.
  2647. --postnzb <prog> : Run <prog> after each NZB file is completed.
  2648. --diskfree <perc> : Stop downloading when dir free space above <perc>
  2649. --redo : Don't skip over existing downloads, do them again
  2650. --insane : Bypass NZB sanity checks completely
  2651. --dropbad : Auto-skip files in the NZBs with suspected broken parts
  2652. --skip <n> : Skip the first <n> files in the nzb (don't process)
  2653. --med <kBps> : Set "med" bandwidth to kBps (default is 95kBps)
  2654. --low <kBps> : Set "low" bandwidth to kBps (default is 35kBps)
  2655. --speed <speed> : Explicitly specify transfer bandwidth in kBps
  2656. --decodelog <file>: Append uudeview output into <file> (default = none)
  2657. --dthreadct <ct> : Use <ct> number of decoder threads. Set ct = 0 for single
  2658. threaded perl operation. (Note: When ct = 0, downloads
  2659. will be paused during file decoding)
  2660. --rcport <port> : Enable remote control functionality on port <port>
  2661. --retrywait <n> : Wait <n> seconds between reconnect tries (default = 300)
  2662. --nosort : Don't sort files by name before processing
  2663. --chunksize : Amount to read on each recv() call (for tweakers only)
  2664. : Default = 5k, Can specify in bytes or kb (ie. 5120 or 5k)
  2665. --filter <regex> : Filter NZB contents on <regex> in subject line
  2666. --ifilter <regex> : Inverse filter NZB contents on <regex> in subject line
  2667. --uudeview <app> : Specify full path to uudeview (default found in \$PATH)
  2668. --tfuser : TF username to run as (required)
  2669. --help : Show this screen
  2670. nzbperl version $version, Copyright (C) 2004 Jason Plumb
  2671. nzbperl comes with ABSOLUTELY NO WARRANTY; This is free software, and
  2672. you are welcome to redistribute it under certain conditions; Please
  2673. see the source for additional details.
  2674. this version is rewritten to be used with torrentflux-b4rt and cannot work
  2675. in standalone mode.
  2676. EOL
  2677. ;
  2678. if($errmsg and (length($errmsg))){
  2679. print " *****************************************************************\n";
  2680. print " ERROR:\n";
  2681. print " $errmsg\n";
  2682. print " *****************************************************************\n";
  2683. }
  2684. }
  2685. #------------------------------------------------------------------------------#
  2686. # Sub: getStatSpeed #
  2687. # Arguments: null #
  2688. # Returns: down-speed formatted for stat-file #
  2689. #------------------------------------------------------------------------------#
  2690. sub getStatSpeed {
  2691. my $sumbps = 0;
  2692. foreach my $i (1..$connct){
  2693. my $c = $conn[$i-1];
  2694. next unless $c->{'file'}; # skip inactive connections
  2695. $sumbps += ($c->{'filebytes'} - $c->{'bwstartbytes'})/Time::HiRes::tv_interval($c->{'bwstarttime'});
  2696. }
  2697. return sprintf("%0.2f %s", ($sumbps / 1024), "kB/s");
  2698. }
  2699. #------------------------------------------------------------------------------#
  2700. # Sub: writeStatStartup #
  2701. # Arguments: null #
  2702. # Returns: return-value of write #
  2703. #------------------------------------------------------------------------------#
  2704. sub writeStatStartup {
  2705. # set some values
  2706. $sf->set("running", 1);
  2707. $sf->set("percent_done", 0);
  2708. $sf->set("time_left", "Starting...");
  2709. $sf->set("down_speed", "0.00 kB/s");
  2710. $sf->set("up_speed", "0.00 kB/s");
  2711. $sf->set("transferowner", $tfuser);
  2712. $sf->set("seeds", 1);
  2713. $sf->set("peers", 1);
  2714. $sf->set("sharing", "");
  2715. $sf->set("seedlimit", "");
  2716. $sf->set("uptotal", 0);
  2717. $sf->set("downtotal", 0);
  2718. $sf->set("size", $totalsCopy{'total size'});
  2719. # write
  2720. return $sf->write();
  2721. }
  2722. #------------------------------------------------------------------------------#
  2723. # Sub: writeStatRunning #
  2724. # Arguments: null #
  2725. # Returns: return-value of write #
  2726. #------------------------------------------------------------------------------#
  2727. sub writeStatRunning {
  2728. # set some values
  2729. $sf->set("percent_done", $totals{'total size'} == 0 ? 0 : int(100.0 * $totals{'total bytes'} / $totals{'total size'}));
  2730. $sf->set("time_left", getETA());
  2731. $sf->set("down_speed", getStatSpeed());
  2732. $sf->set("downtotal", $totals{'total bytes'});
  2733. # write
  2734. return $sf->write();
  2735. }
  2736. #------------------------------------------------------------------------------#
  2737. # Sub: writeStatShutdown #
  2738. # Arguments: null #
  2739. # Returns: return-value of write #
  2740. #------------------------------------------------------------------------------#
  2741. sub writeStatShutdown {
  2742. # set some values
  2743. $sf->set("running", 0);
  2744. if ($noMoreWorkTodo) {
  2745. # done
  2746. $sf->set("percent_done", 100);
  2747. $sf->set("time_left", "Download Succeeded!");
  2748. } else {
  2749. # stopped
  2750. $sf->set("percent_done", $totals{'total size'} == 0 ? "-100" : ((int(100.0 * $totals{'total bytes'} / $totals{'total size'})) + 100) * (-1));
  2751. $sf->set("time_left", "Transfer Stopped");
  2752. }
  2753. $sf->set("down_speed", "");
  2754. $sf->set("up_speed", "");
  2755. $sf->set("transferowner", $tfuser);
  2756. $sf->set("seeds", "");
  2757. $sf->set("peers", "");
  2758. $sf->set("sharing", "");
  2759. $sf->set("seedlimit", "");
  2760. $sf->set("uptotal", 0);
  2761. $sf->set("downtotal", $totals{'total bytes'});
  2762. $sf->set("size", $totalsCopy{'total size'});
  2763. # write
  2764. return $sf->write();
  2765. }
  2766. #------------------------------------------------------------------------------#
  2767. # Sub: pidFileWrite #
  2768. # Arguments: null #
  2769. # Returns: null #
  2770. #------------------------------------------------------------------------------#
  2771. sub pidFileWrite {
  2772. printMessage("writing pid-file ".$file_pid." (pid: ".$$.")\n");
  2773. open(PIDFILE,">$file_pid");
  2774. print PIDFILE $$."\n";
  2775. close(PIDFILE);
  2776. }
  2777. #------------------------------------------------------------------------------#
  2778. # Sub: pidFileDelete #
  2779. # Arguments: null #
  2780. # Returns: return-val of delete #
  2781. #------------------------------------------------------------------------------#
  2782. sub pidFileDelete {
  2783. printMessage("deleting pid-file ".$file_pid."\n");
  2784. return unlink($file_pid);
  2785. }
  2786. #------------------------------------------------------------------------------#
  2787. # Sub: printMessage #
  2788. # Arguments: message #
  2789. # Return: null #
  2790. #------------------------------------------------------------------------------#
  2791. sub printMessage {
  2792. my $message = shift;
  2793. print STDOUT FluxCommon::getTimeStamp()." ".$message;
  2794. }
  2795. #------------------------------------------------------------------------------#
  2796. # Sub: printError #
  2797. # Arguments: message #
  2798. # Return: null #
  2799. #------------------------------------------------------------------------------#
  2800. sub printError {
  2801. my $message = shift;
  2802. print STDERR FluxCommon::getTimeStamp()." ".$message;
  2803. }
  2804. #------------------------------------------------------------------------------#
  2805. # Sub: processCommandStack #
  2806. # Arguments: Null #
  2807. # Return: 1|0 #
  2808. #------------------------------------------------------------------------------#
  2809. sub processCommandStack {
  2810. # check for command-file
  2811. if (!(-e $file_cmd)) {
  2812. return 0;
  2813. }
  2814. # process the command file
  2815. printMessage("Processing command-file ".$file_cmd."...\n");
  2816. # sep + open file
  2817. my $lineSep = $/;
  2818. undef $/;
  2819. open(CMDFILE,"<$file_cmd");
  2820. # read data
  2821. my $content = <CMDFILE>;
  2822. # close file + sep
  2823. close(CMDFILE);
  2824. $/ = $lineSep;
  2825. # delete file
  2826. unlink($file_cmd);
  2827. # process data
  2828. my @contentary = split(/\n/, $content);
  2829. my $commandCount = 0;
  2830. foreach my $command (@contentary) {
  2831. # exec command
  2832. my $result = execCommand($command);
  2833. if ($result == 1) {
  2834. return 1;
  2835. } elsif ($result == 0) {
  2836. $commandCount++;
  2837. }
  2838. }
  2839. if ($commandCount == 0) {
  2840. printMessage("No commands found.\n");
  2841. }
  2842. return 0;
  2843. }
  2844. #------------------------------------------------------------------------------#
  2845. # Sub: execCommand #
  2846. # Arguments: command #
  2847. # Return: -1|0|1 #
  2848. #------------------------------------------------------------------------------#
  2849. sub execCommand {
  2850. my $command = shift;
  2851. chomp $command;
  2852. $_ = $command;
  2853. SWITCH: {
  2854. /^q$/ && do {
  2855. # quit
  2856. printMessage("command: stop-request, setting shutdown-flag...\n");
  2857. $quitnow = 1;
  2858. return 1;
  2859. };
  2860. /^d(\d+)/ && do {
  2861. # set download speed
  2862. printMessage("command: setting Download-Rate to ".$1."\n");
  2863. $targkBps = $1;
  2864. return 0;
  2865. };
  2866. # default
  2867. printMessage("command unknown or invalid. op-code : ".substr($command, 0 , 1)."\n");
  2868. } # SWITCH
  2869. return -1;
  2870. }
  2871. #------------------------------------------------------------------------------#
  2872. # Sub: shutdownClient #
  2873. # Arguments: Null #
  2874. # Return: Null #
  2875. #------------------------------------------------------------------------------#
  2876. sub shutdownClient {
  2877. # write stat-file
  2878. writeStatShutdown();
  2879. # delete pid-file
  2880. pidFileDelete();
  2881. # exit message
  2882. printMessage("nzbperl exit.\n");
  2883. # exit
  2884. exit;
  2885. }
  2886. #------------------------------------------------------------------------------#
  2887. # Sub: gotSigHup #
  2888. # Arguments: Null #
  2889. # Returns: Null #
  2890. #------------------------------------------------------------------------------#
  2891. sub gotSigHup {
  2892. printMessage("got SIGHUP, ignoring...\n");
  2893. }
  2894. #------------------------------------------------------------------------------#
  2895. # Sub: gotSigInt #
  2896. # Arguments: null #
  2897. # Returns: null #
  2898. #------------------------------------------------------------------------------#
  2899. sub gotSigInt {
  2900. printMessage("got SIGINT, setting shutdown-flag...\n");
  2901. $quitnow = 1;
  2902. }
  2903. #------------------------------------------------------------------------------#
  2904. # Sub: gotSigTerm #
  2905. # Arguments: null #
  2906. # Returns: null #
  2907. #------------------------------------------------------------------------------#
  2908. sub gotSigTerm {
  2909. printMessage("got SIGTERM, setting shutdown-flag...\n");
  2910. $quitnow = 1;
  2911. }
  2912. #------------------------------------------------------------------------------#
  2913. # Sub: gotSigQuit #
  2914. # Arguments: null #
  2915. # Returns: null #
  2916. #------------------------------------------------------------------------------#
  2917. sub gotSigQuit {
  2918. printMessage("got SIGQUIT, setting shutdown-flag...\n");
  2919. $quitnow = 1;
  2920. }