transmission.pl 23 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001
  1. #!/usr/bin/perl
  2. use warnings;
  3. use strict;
  4. # Includes
  5. use JSON;
  6. use Date::Parse;
  7. use File::Copy;
  8. use File::Touch;
  9. use File::Temp 'mktemp';
  10. use File::Path 'remove_tree';
  11. use File::Basename;
  12. use Image::ExifTool qw/ :Public/;
  13. use FindBin qw($Bin);
  14. use lib $Bin;
  15. use Fetch;
  16. # Prototypes
  17. sub session($);
  18. sub fetch($);
  19. sub getSSE($);
  20. sub getDest($$$$);
  21. sub storeFiles($$$$);
  22. sub readNZB($$);
  23. sub storeNZB($);
  24. sub storeTor($);
  25. sub delNZB($);
  26. sub delTor($);
  27. sub guessExt($);
  28. sub processFile($$$);
  29. sub unrar($$);
  30. sub seriesCleanupLocal($);
  31. sub seriesCleanupCore($);
  32. sub seriesCleanup($);
  33. sub readDir($$);
  34. sub findMediaFiles($$);
  35. sub available($);
  36. # Parameters
  37. my $maxAge = 2.5 * 86400;
  38. my $tvDir = `~/bin/video/mediaPath` . '/TV';
  39. my $monitoredExec = $ENV{'HOME'} . '/bin/video/torrentMonitored.pl';
  40. my $content =
  41. '{"method":"torrent-get","arguments":{"fields":["hashString","id","addedDate","comment","creator",'
  42. . '"dateCreated","isPrivate","name","totalSize","pieceCount","pieceSize","downloadedEver","error","errorString",'
  43. . '"eta","haveUnchecked","haveValid","leftUntilDone","metadataPercentComplete","peersConnected","peersGettingFromUs",'
  44. . '"peersSendingToUs","rateDownload","rateUpload","recheckProgress","sizeWhenDone","status","trackerStats",'
  45. . '"uploadedEver","uploadRatio","seedRatioLimit","seedRatioMode","downloadDir","files","fileStats"]}}';
  46. my $delContent = '{"method":"torrent-remove","arguments":{"ids":["#_ID_#"], "delete-local-data":"true"}';
  47. my $history = '{"method":"history","params":[]}';
  48. my $delHistory = '{"method":"editqueue","params":["historyFinalDelete","",[#_ID_#]]}';
  49. my $delSleep = 2;
  50. my $RAR_MIN_FILES = 4;
  51. my $FIND_MAX_DEPTH = 5;
  52. my $CONF_FILE = $ENV{'HOME'} . '/.download.config';
  53. # Environment
  54. my $DEBUG = 0;
  55. if ($ENV{'DEBUG'}) {
  56. if ($ENV{'DEBUG'} =~ /(\d+)/) {
  57. $DEBUG = $1;
  58. } else {
  59. $DEBUG = 1;
  60. }
  61. }
  62. my $DELETE = 0;
  63. if ($ENV{'DELETE'}) {
  64. if ($DEBUG) {
  65. print STDERR "Failure mode: DELETE\n";
  66. }
  67. $DELETE = 1;
  68. }
  69. # Command-line parameters
  70. my ($hash) = @ARGV;
  71. # Config file
  72. my %CONFIG = ();
  73. if ($CONF_FILE && -r $CONF_FILE) {
  74. my $fh;
  75. open($fh, '<', $CONF_FILE)
  76. or die('Unable to open config file: ' . $CONF_FILE . ': ' . $! . "\n");
  77. while (<$fh>) {
  78. # Skip blank lines and comments
  79. if (/^\s*#/ || /^\s*$/) {
  80. next;
  81. }
  82. # Assume everything else is bash variables
  83. if (my ($key, undef(), $val) = $_ =~ /^\s*(\S[^\=]+)\=([\'\"])(.*)\g{-2}\s*$/) {
  84. $CONFIG{$key} = $val;
  85. if ($DEBUG > 1) {
  86. print STDERR 'Adding config: ' . $key . ' => ' . $val . "\n";
  87. }
  88. } else {
  89. warn('Ignoring config line: ' . $_ . "\n");
  90. }
  91. }
  92. close($fh);
  93. }
  94. # Init
  95. my $fetch = undef();
  96. # Tranmission
  97. if (available($CONFIG{'TRANS_URL'})) {
  98. # Fetch the list of paused (i.e. completed) torrents
  99. $fetch->url($CONFIG{'TRANS_URL'});
  100. $fetch->post_content($content);
  101. &fetch($fetch);
  102. my $torrents = decode_json(scalar($fetch->content()));
  103. $torrents = $torrents->{'arguments'}->{'torrents'};
  104. # Process
  105. if ($DEBUG) {
  106. print STDERR "Processing torrents...\n";
  107. }
  108. foreach my $tor (@{$torrents}) {
  109. # Skip torrents that aren't yet done
  110. if ($tor->{'leftUntilDone'} > 0 || $tor->{'metadataPercentComplete'} < 1) {
  111. next;
  112. }
  113. # If a hash is provided, process only that file
  114. if (defined($hash) && $tor->{'hashString'} ne $hash) {
  115. if ($DEBUG) {
  116. printf STDERR 'Skipping torrent with non-matching hash: ' . $tor->{'name'} . ' (' . $tor->{'hashString'} . ")\n";
  117. }
  118. next;
  119. }
  120. # Process this torrent
  121. if ($DEBUG) {
  122. print STDERR 'Processing completed torrent: ' . $tor->{'name'} . "\n";
  123. }
  124. if (storeTor($tor) || $DELETE) {
  125. delTor($tor);
  126. } else {
  127. print STDERR 'Unable to store torrent: ' . $tor->{'name'} . "\n";
  128. }
  129. next;
  130. }
  131. # NZB
  132. if (available($CONFIG{'NZB_URL'})) {
  133. # Fetch the list of historical (i.e. completed) NZBs
  134. $fetch->url($CONFIG{'NZB_URL'});
  135. $fetch->post_content($history);
  136. $fetch->fetch();
  137. my $resp = decode_json(scalar($fetch->content()));
  138. if (!defined($resp) || ref($resp) ne 'HASH' || !exists($resp->{'result'}) || ref($resp->{'result'}) ne 'ARRAY') {
  139. print STDERR 'Invalid NZB response: ' . $fetch->content() . "\n";
  140. }
  141. # Process
  142. if ($DEBUG) {
  143. print STDERR "Processing NZBs...\n";
  144. }
  145. foreach my $data (@{ $resp->{'result'} }) {
  146. my $nzb = undef();
  147. if (!readNZB(\$nzb, $data)) {
  148. warn("Unable to parse NZB\n");
  149. next;
  150. }
  151. # If a hash is provided, process only that file
  152. #if (defined($hash) && $nzb->{'has'} ne $hash) {
  153. # if ($DEBUG) {
  154. # printf STDERR 'Skipping NZB with non-matching hash: ' . $nzb->{'name'} . ' (' . $nzb->{'hash'} . ")\n";
  155. # }
  156. # next;
  157. #}
  158. # Delete failed NZBs
  159. if ($nzb->{'status'} =~ /FAILURE\/(?:HEALTH|PAR|UNPACK|FETCH|SCAN)/ || $nzb->{'status'} eq 'DELETED/MANUAL') {
  160. delNZB($nzb);
  161. next;
  162. }
  163. # Process sucessful NZBs
  164. if ($nzb->{'status'} =~ /SUCCESS\/(?:UNPACK|HEALTH)/) {
  165. if (storeNZB($nzb) || $DELETE) {
  166. delNZB($nzb);
  167. } else {
  168. print STDERR 'Unable to store NZB: ' . $nzb->{'name'} . "\n";
  169. }
  170. next;
  171. }
  172. }
  173. }
  174. }
  175. # Cleanup
  176. exit(0);
  177. # Grab the session ID
  178. sub session($) {
  179. my ($fetch) = @_;
  180. my %headers = ();
  181. ($headers{'X-Transmission-Session-Id'}) = $fetch->content() =~ /X\-Transmission\-Session\-Id\:\s+(\w+)/;
  182. $fetch->headers(\%headers);
  183. }
  184. sub fetch($) {
  185. my ($fetch) = @_;
  186. $fetch->fetch('nocheck' => 1);
  187. if ($fetch->status_code() != 200) {
  188. &session($fetch);
  189. $fetch->fetch();
  190. if ($fetch->status_code() != 200) {
  191. die('Unable to fetch: ' . $fetch->status_code() . "\n");
  192. }
  193. }
  194. }
  195. sub getSSE($) {
  196. my ($name) = @_;
  197. if ($DEBUG) {
  198. print STDERR 'Finding series/season/episode for: ' . $name . "\n";
  199. }
  200. my $season = 0;
  201. my $episode = 0;
  202. my $seasonBlock = '';
  203. if ($name =~ /(?:\b|_)(20\d\d(?:\.|\-)[01]?\d(?:\.|\-)[0-3]?\d)(?:\b|_)/) {
  204. $seasonBlock = $1;
  205. my ($month, $day);
  206. ($season, $month, $day) = $seasonBlock =~ /(20\d\d)(?:\.|\-)([01]?\d)(?:\.|\-)([0-3]?\d)/;
  207. $season = int($season);
  208. $episode = sprintf('%04d-%02d-%02d', $season, $month, $day);
  209. } elsif ($name =~ /(?:\b|_)(S(?:eason)?[_\s\.\-]*\d{1,2}[_\s\.\-]*E(?:pisode)?[_\s\.]*\d{1,3}(?:E\d{1,3})?)(?:\b|_)/i) {
  210. $seasonBlock = $1;
  211. ($season, $episode) = $seasonBlock =~ /S(?:eason)?[_\s\.\-]*(\d{1,2})[_\s\.\-]*E(?:pisode)?[_\s\.\-]*(\d{1,3})/i;
  212. $season = int($season);
  213. $episode = sprintf('%02d', int($episode));
  214. } elsif ($name =~ /\b(\d{1,2}x\d{2,3})\b/i) {
  215. $seasonBlock = $1;
  216. ($season, $episode) = $seasonBlock =~ /(\d+)x(\d+)/i;
  217. $season = int($season);
  218. $episode = sprintf('%02d', int($episode));
  219. } elsif ($name =~ /(?:\b|_)([01]?\d[_\s\.]?[0-3]\d)(?:\b|_)/i) {
  220. $seasonBlock = $1;
  221. ($season, $episode) = $seasonBlock =~ /(?:\b|_)([01]?\d)[_\s\.]?([0-3]\d)/i;
  222. $season = int($season);
  223. $episode = sprintf('%02d', int($episode));
  224. }
  225. if (!defined($seasonBlock) || $season < 1 || length($episode) < 1 || $episode eq '0') {
  226. if ($DEBUG) {
  227. print STDERR 'Could not find seasonBlock in: ' . $name . "\n";
  228. }
  229. return;
  230. }
  231. # Assume the series titles comes before the season/episode block
  232. my $series = '';
  233. my $sIndex = index($name, $seasonBlock);
  234. if ($sIndex > 0) {
  235. $series = substr($name, 0, $sIndex - 1);
  236. $series = seriesCleanupCore($series);
  237. }
  238. if ($DEBUG) {
  239. print STDERR 'Series: ' . $series . ' Season: ' . $season . ' Episode: ' . $episode . "\n";
  240. }
  241. return ($series, $season, $episode);
  242. }
  243. sub getDest($$$$) {
  244. my ($series, $season, $episode, $ext) = @_;
  245. if ($DEBUG) {
  246. print STDERR 'Finding destination for: ' . $series . ' S' . $season . 'E' . $episode . "\n";
  247. }
  248. # Input cleanup
  249. $episode =~ s/^0+//;
  250. # Find all our existing TV series
  251. my @shows = ();
  252. my %showsCan = ();
  253. open(SHOWS, '-|', $monitoredExec, 'STORE')
  254. or die("Unable to fork: ${!}\n");
  255. while (<SHOWS>) {
  256. chomp;
  257. my $orig = $_;
  258. my $clean = seriesCleanupLocal($_);
  259. push(@shows, $clean);
  260. $showsCan{$clean} = $orig;
  261. }
  262. close SHOWS or die("Unable to read monitored series list: ${!} ${?}\n");
  263. # See if we can find a unique series name match
  264. my $sClean = seriesCleanup($series);
  265. my $sCleanCore = seriesCleanupCore($series);
  266. my $sMatch = '\b' . $sClean . '\b';
  267. $series = '';
  268. foreach my $show (@shows) {
  269. # Match the folder name or the search_name
  270. my $coarse_match = 0;
  271. if ($show =~ /${sMatch}/i) {
  272. $coarse_match = 1;
  273. } else {
  274. my $search_name = '';
  275. my $file = $tvDir . '/' . $showsCan{$show} . '/search_name';
  276. if (-e $file) {
  277. open(FH, $file)
  278. or die('Unable to read search_name file: ' . $file . "\n");
  279. $search_name = <FH>;
  280. close(FH);
  281. }
  282. if ($search_name) {
  283. $search_name = seriesCleanup($search_name);
  284. if ($search_name =~ /${sMatch}/i) {
  285. $coarse_match = 1;
  286. }
  287. }
  288. }
  289. # If we got a course match, continue checking
  290. if ($coarse_match) {
  291. # Enforce any secondary matching rules (for ambiguous titles)
  292. my $detail_match = 1;
  293. {
  294. my @lines;
  295. my $file = $tvDir . '/' . $showsCan{$show} . '/must_match';
  296. if (-e $file) {
  297. local ($/, *FH);
  298. open(FH, $file)
  299. or die('Unable to read must_match file: ' . $file . "\n");
  300. my @tmp = <FH>;
  301. close(FH);
  302. push(@lines, @tmp);
  303. }
  304. foreach my $line (@lines) {
  305. chomp($line);
  306. if (!eval('$sCleanCore =~ m' . $line)) {
  307. if ($DEBUG) {
  308. print STDERR 'Skipping ' . $show . ' due to must_match failure for: ' . $line . "\n";
  309. }
  310. $detail_match = 0;
  311. last;
  312. }
  313. }
  314. }
  315. if (!$detail_match) {
  316. next;
  317. }
  318. # If we find more than one matching series
  319. if (length($series) > 0) {
  320. # If the old match is a subset of the new match, prefer the new match
  321. # If the new match is a subet of the old match, prefer the old match
  322. # Bail if the matches do not overlap
  323. if ($show =~ m/^${series}\b/) {
  324. if ($DEBUG) {
  325. print STDERR 'Using longer match ('
  326. . $showsCan{$show}
  327. . ') in place of shorter match ('
  328. . $showsCan{$series} . ")\n";
  329. }
  330. $series = $show;
  331. } elsif ($series =~ m/^${show}\b/) {
  332. if ($DEBUG) {
  333. print STDERR 'Using longer match ('
  334. . $showsCan{$series}
  335. . ') in place of shorter match ('
  336. . $showsCan{$show} . ")\n";
  337. }
  338. $series = $series;
  339. } else {
  340. print STDERR 'Matched both ' . $showsCan{$series} . ' and ' . $showsCan{$show} . "\n";
  341. return;
  342. }
  343. } else {
  344. # If we're still around this is the first series title match
  345. $series = $show;
  346. }
  347. }
  348. }
  349. if (length($series) < 1) {
  350. if ($DEBUG) {
  351. print STDERR 'No series match for: ' . $sMatch . "\n";
  352. }
  353. return;
  354. }
  355. # Lookup the canonical name from the matched name
  356. $series = $showsCan{$series};
  357. # Make sure we have the right season folder
  358. my $seriesDir = $tvDir . '/' . $series;
  359. my $seasonDir = $seriesDir . '/' . 'Season ' . $season;
  360. if (!-d $seasonDir) {
  361. print STDERR 'No season folder for: ' . $series . ' S' . $season . "\n";
  362. return;
  363. }
  364. # Bail if we already have this episode
  365. my @episodes = readDir($seasonDir, 0);
  366. foreach my $ep (@episodes) {
  367. my ($epNum) = $ep =~ /^\s*([\d\-]+)\s*\-\s*/;
  368. if (!defined($epNum)) {
  369. next;
  370. }
  371. $epNum =~ s/^0+//;
  372. if ($epNum eq $episode) {
  373. print STDERR 'Existing episode for: ' . $series . ' S' . $season . 'E' . $episode . "\n";
  374. return;
  375. }
  376. }
  377. # Construct the final path
  378. if (!defined($ext) || length($ext) < 1) {
  379. $ext = 'avi';
  380. }
  381. if ($episode =~ /^\d+$/) {
  382. $episode = sprintf('%02d', $episode);
  383. }
  384. my $dest = sprintf('%s/%s - NoName.%s', $seasonDir, $episode, $ext);
  385. if ($DEBUG) {
  386. print STDERR 'Destination: ' . $dest . "\n";
  387. }
  388. return $dest;
  389. }
  390. sub storeFiles($$$$) {
  391. my ($path, $files, $name, $id) = @_;
  392. my $failure = 0;
  393. foreach my $file (@{$files}) {
  394. my $result = -1;
  395. if (defined($file) && length($file) > 0 && -r $file) {
  396. $result = &processFile($file, $path, $id);
  397. }
  398. if (defined($result) && $result == 1) {
  399. if ($DEBUG) {
  400. print STDERR 'File stored successfully: ' . $name . "\n";
  401. }
  402. } elsif (defined($result) && $result == -1) {
  403. if ($DEBUG) {
  404. print STDERR 'Refusing to save bad download: ' . $name . "\n";
  405. }
  406. last;
  407. } else {
  408. warn('Error storing file "' . basename($file) . '" from: ' . $name . "\n");
  409. $failure = 1;
  410. }
  411. }
  412. return $failure ? 0 : 1;
  413. }
  414. sub readNZB($$) {
  415. my ($nzb, $data) = @_;
  416. my %tmp = ('id' => undef(), 'name' => undef(), 'status' => undef(), 'path' => undef());
  417. # Sanity
  418. if (ref($data) ne 'HASH') {
  419. print STDERR 'Invalid NZB item: ' . $data . "\n";
  420. return 0;
  421. }
  422. # ID
  423. if (exists($data->{'ID'})) {
  424. $tmp{'id'} = int($data->{'ID'});
  425. }
  426. # Name
  427. if (exists($data->{'Name'})) {
  428. $tmp{'name'} = $data->{'Name'};
  429. }
  430. if (!$tmp{'name'} && exists($data->{'NZBName'})) {
  431. $tmp{'name'} = $data->{'NZBName'};
  432. }
  433. if (!$tmp{'name'} && exists($data->{'NZBNicename'})) {
  434. $tmp{'name'} = $data->{'NZBNicename'};
  435. }
  436. if (!$tmp{'name'}) {
  437. print STDERR 'Unable to retrieve name for: ' . $tmp{'id'} . "\n";
  438. return 0;
  439. }
  440. # Status
  441. if (exists($data->{'Status'})) {
  442. $tmp{'status'} = $data->{'Status'};
  443. }
  444. if (!$tmp{'status'}) {
  445. print STDERR 'Unable to retrieve status for: ' . $tmp{'id'} . "\n";
  446. return 0;
  447. }
  448. # Path
  449. if (exists($data->{'DestDir'})) {
  450. $tmp{'path'} = $data->{'DestDir'};
  451. }
  452. if (!$tmp{'path'} && $DEBUG) {
  453. print STDERR 'Unable to retrieve path for: ' . $tmp{'id'} . "\n";
  454. }
  455. # Debug
  456. if ($DEBUG > 1) {
  457. print STDERR "\n\n";
  458. foreach my $key (keys(%tmp)) {
  459. print STDERR $key . ' => ' . $tmp{$key} . "\n";
  460. }
  461. }
  462. # Store the NZB hash and return success
  463. ${$nzb} = \%tmp;
  464. return 1;
  465. }
  466. sub storeTor($) {
  467. my ($tor) = @_;
  468. if ($DEBUG) {
  469. print STDERR 'Storing torrent: ' . $tor->{'name'} . "\n";
  470. }
  471. # Handle individual files and directories
  472. my @files = ();
  473. my @newFiles = ();
  474. my $path = $tor->{'downloadDir'} . '/' . $tor->{'name'};
  475. if (!-d $path) {
  476. push(@files, $path);
  477. } else {
  478. # State variables
  479. my $IS_RAR = '';
  480. my $IS_FAKE = 0;
  481. # Look for RAR files
  482. foreach my $file (@{ $tor->{'files'} }) {
  483. if ($file->{'name'} =~ /\.rar$/i) {
  484. $IS_RAR = $tor->{'downloadDir'} . '/' . $file->{'name'};
  485. last;
  486. }
  487. }
  488. # Check for password files
  489. foreach my $file (@{ $tor->{'files'} }) {
  490. if ($file->{'name'} =~ /passw(?:or)?d/i) {
  491. print STDERR 'Password file detected in: ' . $tor->{'name'} . "\n";
  492. $IS_FAKE = 1;
  493. last;
  494. }
  495. }
  496. # Check for single-file RARs
  497. if ($IS_RAR && scalar(@{ $tor->{'files'} }) < $RAR_MIN_FILES) {
  498. print STDERR 'Single-file RAR detected in: ' . $tor->{'name'} . "\n";
  499. $IS_FAKE = 1;
  500. }
  501. # Delete fake torrents and bail
  502. if ($IS_FAKE) {
  503. if ($DEBUG) {
  504. print STDERR 'Deleting fake torrent: ' . $tor->{'name'} . "\n";
  505. }
  506. delTor($tor);
  507. next;
  508. }
  509. # Unrar if necessary
  510. if (defined($IS_RAR) && length($IS_RAR) > 0) {
  511. @newFiles = &unrar($IS_RAR, $path);
  512. }
  513. # Find all media files, recursively
  514. @files = findMediaFiles($path, 0);
  515. }
  516. # Process any files we found
  517. if (scalar(@files) < 1) {
  518. print STDERR 'No media files found in torrent: ' . $tor->{'name'} . "\n";
  519. return 0;
  520. }
  521. my $retval = storeFiles($path, \@files, $tor->{'name'}, $tor->{'id'});
  522. # Delete any files we added
  523. foreach my $file (@newFiles) {
  524. if ($DEBUG) {
  525. print STDERR 'Deleting new file: ' . basename($file) . "\n";
  526. }
  527. unlink($file);
  528. }
  529. # Return whatever storeFiles() said
  530. return $retval;
  531. }
  532. sub storeNZB($) {
  533. my ($nzb) = @_;
  534. if ($DEBUG) {
  535. print STDERR 'Storing NZB: ' . $nzb->{'name'} . "\n";
  536. }
  537. # Find all media files, recursively
  538. my @files = findMediaFiles($nzb->{'path'}, 0);
  539. # Process any files we found
  540. if (scalar(@files) < 1) {
  541. print STDERR 'No media files found in NZB: ' . $nzb->{'name'} . "\n";
  542. return undef();
  543. }
  544. return storeFiles($nzb->{'path'}, \@files, $nzb->{'name'}, $nzb->{'id'});
  545. }
  546. sub delNZB($) {
  547. my ($nzb) = @_;
  548. if ($DEBUG) {
  549. print STDERR 'Deleting NZB: ' . $nzb->{'name'} . "\n";
  550. }
  551. # Remove the files, if any
  552. if (-e $nzb->{'path'} && -d $nzb->{'path'}) {
  553. if ($DEBUG) {
  554. print STDERR 'Removing NZB folder: ' . $nzb->{'path'} . "\n";
  555. }
  556. remove_tree($nzb->{'path'});
  557. # Check for deletion
  558. if (-e $nzb->{'path'}) {
  559. print STDERR 'Unable to remove NZB folder: ' . $nzb->{'path'} . "\n";
  560. return 0;
  561. }
  562. }
  563. # Send the command
  564. my $id = $nzb->{'id'};
  565. my $content = $delHistory;
  566. $content =~ s/\#_ID_\#/${id}/;
  567. $fetch->post_content($content);
  568. $fetch->fetch();
  569. # Check the result
  570. if ($fetch->content() =~ /\"result\" \: true/i) {
  571. return 1;
  572. }
  573. return 0;
  574. }
  575. sub delTor($) {
  576. my ($tor) = @_;
  577. if ($DEBUG) {
  578. print STDERR 'Deleting torrent: ' . $tor->{'name'} . "\n";
  579. }
  580. # Sleep to avoid bugs in Transmission (i.e. multiple deletes fail)
  581. sleep($delSleep);
  582. # Construct the content
  583. my $id = $tor->{'hashString'};
  584. my $content = $delContent;
  585. $content =~ s/\#_ID_\#/${id}/;
  586. # Send the command
  587. &session($fetch);
  588. $fetch->post_content($content);
  589. &fetch($fetch);
  590. }
  591. sub guessExt($) {
  592. my ($file) = @_;
  593. my $ext = '';
  594. # Believe most non-avi file extensions without checking
  595. # It's mostly "avi" that lies, and the checks are expensive
  596. {
  597. my $orig_ext = basename($file);
  598. $orig_ext =~ s/^.*\.(\w{2,3})$/$1/;
  599. $orig_ext = lc($orig_ext);
  600. if ($orig_ext eq 'mkv' || $orig_ext eq 'ts') {
  601. $ext = $orig_ext;
  602. }
  603. }
  604. # Deal with the weird case of text files labeled as .txt.mp4
  605. if ($file =~ /\.txt\.mp4$/i) {
  606. $ext = 'txt';
  607. }
  608. # Guess the file type if we don't have one yet
  609. if (!$ext) {
  610. my $mime = '';
  611. my $demux = '';
  612. # Try ExifTool for MIME and demuxer types
  613. if (!$mime || !$demux) {
  614. my $exif = ImageInfo($file);
  615. if (defined($exif) && ref($exif) eq 'HASH') {
  616. if ($exif->{'MIMEType'}) {
  617. $mime = $exif->{'MIMEType'};
  618. }
  619. if ($exif->{'FileType'}) {
  620. $demux = $exif->{'FileType'};
  621. }
  622. }
  623. }
  624. # Try file(1) for MIME types
  625. if (!$mime) {
  626. open(FILE, '-|', 'file', '-b', $file);
  627. while (<FILE>) {
  628. $mime .= $_;
  629. }
  630. close(FILE);
  631. }
  632. # Cleanup the demuxer and MIME types
  633. $demux =~ s/^\s+//;
  634. $demux =~ s/\s+$//;
  635. $mime =~ s/^\s+//;
  636. $mime =~ s/\s+$//;
  637. $mime =~ s/\;.*$//;
  638. $mime =~ s/^video\///i;
  639. # Try to pick an extension we understand
  640. if ($demux =~ /MKV/i || $mime =~ /Matroska/i) {
  641. $ext = 'mkv';
  642. } elsif ($demux =~ /ASF/i || $mime =~ /\bASF\b/i) {
  643. $ext = 'wmv';
  644. } elsif ($demux =~ /MP4/i || $mime =~ /\bMP4\b/i) {
  645. $ext = 'mp4';
  646. } elsif ($mime =~ /\bZIP/i) {
  647. $ext = 'zip';
  648. } elsif ($demux =~ /MPEGTS/i) {
  649. $ext = 'ts';
  650. } else {
  651. $ext = 'avi';
  652. }
  653. }
  654. return $ext;
  655. }
  656. sub processFile($$$) {
  657. my ($file, $path, $id) = @_;
  658. if ($DEBUG) {
  659. print STDERR 'Attempting to process file: ' . basename($file) . "\n";
  660. }
  661. # Skip known packaging files files with success code
  662. if ($file =~ /\/RARBG\.com\.(?:mp4|avi|mkv|txt)$/i || $file =~ /\.txt\.(?:mp4|avi|mkv)$/) {
  663. if ($DEBUG) {
  664. print STDERR 'Declining to save packaging file: ' . $file . "\n";
  665. }
  666. return 1;
  667. }
  668. # Guess a file extension and basename
  669. my $ext = &guessExt($file);
  670. my $filename = basename($file);
  671. # Delete WMV files -- mostly viruses/fake
  672. if ($ext =~ /wmv/i) {
  673. if ($DEBUG) {
  674. print STDERR 'Declining to save WMV file: ' . $filename . "\n";
  675. }
  676. return -1;
  677. }
  678. # Delete ZIP files -- mostly fake
  679. if ($ext =~ /zip/i) {
  680. if ($DEBUG) {
  681. print STDERR 'Declining to save ZIP file: ' . $filename . "\n";
  682. }
  683. return -1;
  684. }
  685. # Allow multiple guesses at the series/season/episode
  686. my $dest = '';
  687. LOOP: {
  688. # Determine the series, season, and episode number
  689. my ($series, $season, $episode) = &getSSE($filename);
  690. if ( defined($series)
  691. && length($series) > 0
  692. && defined($season)
  693. && $season > 0
  694. && defined($episode)
  695. && length($episode) > 0
  696. && $episode ne '0')
  697. {
  698. # Find the proper destination for this torrent, if any
  699. $dest = &getDest($series, $season, $episode, $ext);
  700. }
  701. # Sanity/loop check
  702. if (!defined($dest) || length($dest) < 1) {
  703. # If there is no match but the torrent is a folder
  704. # retry the guess using the torrent path
  705. if ($file ne $path) {
  706. my $newName = basename($path);
  707. if ($newName ne $filename) {
  708. $filename = basename($path);
  709. redo LOOP;
  710. }
  711. }
  712. # Otherwise we just fail
  713. print STDERR 'No destination for: ' . basename($file) . ' (' . $id . ")\n";
  714. return;
  715. }
  716. }
  717. # Copy (to let Transmission deal with local cleanup) and rename into place
  718. my $tmp = mktemp($dest . '.XXXXXXXX');
  719. copy($file, $tmp);
  720. rename($tmp, $dest);
  721. # Touch to avoid carrying dates set in the download process
  722. touch($dest);
  723. # Return success
  724. return 1;
  725. }
  726. sub unrar($$) {
  727. my ($file, $path) = @_;
  728. if ($DEBUG) {
  729. print STDERR 'UnRARing: ' . basename($file) . "\n";
  730. }
  731. # Keep a list of old files, so we can find/delete the output
  732. my @beforeFiles = readDir($path, 1);
  733. # Run the unrar utility
  734. my $pid = fork();
  735. if (!defined($pid)) {
  736. print STDERR "Unable to fork for RAR\n";
  737. return;
  738. } elsif ($pid == 0) {
  739. # Child
  740. close(STDOUT);
  741. close(STDERR);
  742. chdir($path);
  743. my @args = ('unrar', 'e', '-p-', '-y', $file);
  744. exec { $args[0] } @args;
  745. }
  746. # Wait for the child
  747. waitpid($pid, 0);
  748. # Compare the old file list to the new one
  749. my @newFiles = ();
  750. my @afterFiles = readDir($path, 1);
  751. foreach my $file (@afterFiles) {
  752. my $found = 0;
  753. foreach my $file2 (@beforeFiles) {
  754. if ($file eq $file2) {
  755. $found = 1;
  756. last;
  757. }
  758. }
  759. if (!$found) {
  760. push(@newFiles, $file);
  761. }
  762. }
  763. # Return the list of added files
  764. return @newFiles;
  765. }
  766. sub seriesCleanupLocal($) {
  767. my ($name) = @_;
  768. $name =~ s/\.//g;
  769. return seriesCleanupCore($name);
  770. }
  771. sub seriesCleanupCore($) {
  772. my ($name) = @_;
  773. $name =~ s/^.*\[[^\]]*\]\s+//;
  774. $name =~ s/\b(?:and|\&)\b/ /ig;
  775. $name =~ s/^\s*The\b//ig;
  776. $name =~ s/\[[^\]]*\]//g;
  777. $name =~ s/\{[^\}]*\}//g;
  778. $name =~ s/[\(\)]//g;
  779. $name =~ s/[\'\"]//g;
  780. $name =~ s/[^\w\s]+/ /g;
  781. $name =~ s/_+/ /g;
  782. $name =~ s/\s+/ /g;
  783. $name =~ s/^\s*//;
  784. $name =~ s/\s*$//;
  785. return $name;
  786. }
  787. sub seriesCleanup($) {
  788. my ($name) = @_;
  789. $name =~ s/\s*\(?US\)?\s*$//;
  790. $name =~ s/\s*\(?20[01][0-9]\)?\s*$//;
  791. $name =~ s/^\s*www\.scenetime\.com(?:\s+\-)?\s*//i;
  792. return seriesCleanupCore($name);
  793. }
  794. sub readDir($$) {
  795. my ($indir, $long) = @_;
  796. # Clean up the input directory
  797. $indir =~ s/\/+$//;
  798. my @files = ();
  799. if (!opendir(DIR, $indir)) {
  800. warn('readDir: Unable to read directory: ' . $indir . ': ' . $! . "\n");
  801. return;
  802. }
  803. foreach my $file (readdir(DIR)) {
  804. my $keep = 0;
  805. # Skip junk
  806. if ( $file eq '.'
  807. || $file eq '..'
  808. || $file =~ /\._/)
  809. {
  810. next;
  811. }
  812. # Save the full path
  813. if ($long) {
  814. push(@files, $indir . '/' . $file);
  815. } else {
  816. push(@files, $file);
  817. }
  818. }
  819. closedir(DIR);
  820. # Sorts the files before returning to the caller
  821. my @orderedFiles = ();
  822. @orderedFiles = sort(@files);
  823. # Return the file list
  824. return (@orderedFiles);
  825. }
  826. sub findMediaFiles($$) {
  827. my ($path, $depth) = @_;
  828. my @files = ();
  829. # Recursion limit
  830. if ($depth > $FIND_MAX_DEPTH) {
  831. return ();
  832. }
  833. my @tmpFiles = readDir($path, 1);
  834. foreach my $file (@tmpFiles) {
  835. # Skip sample files
  836. if ($file =~ /sample/i) {
  837. next;
  838. }
  839. # Recurse on directories
  840. if (-d $file) {
  841. push(@files, findMediaFiles($file, $depth + 1));
  842. next;
  843. }
  844. # Save matching files
  845. if ($file =~ /\.(?:avi|mkv|m4v|mov|mp4|ts|wmv)$/i) {
  846. push(@files, $file);
  847. next;
  848. }
  849. }
  850. return @files;
  851. }
  852. # Check availability of web interface
  853. sub available($) {
  854. my ($url) = @_;
  855. if ($DEBUG) {
  856. print STDERR "Checking for interface\n";
  857. }
  858. # Reset from the last host
  859. $fetch = Fetch->new();
  860. my ($host) = $url =~ /^(\w+\:\/\/[^\/]+\/?)/;
  861. $fetch->url($host);
  862. if ($fetch->fetch('nocheck' => 1) != 200) {
  863. if ($DEBUG) {
  864. print STDERR 'Code: ' . $fetch->status_code() . "\n";
  865. print STDERR 'Content: ' . $fetch->content() . "\n";
  866. warn('Interface not available: ' . $host . "\n");
  867. }
  868. return 0;
  869. }
  870. return 1;
  871. }