transmission.pl 23 KB

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