transmission.pl 24 KB

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