transmission.pl 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759
  1. #!/usr/bin/perl
  2. use warnings;
  3. use strict;
  4. # Includes
  5. use JSON;
  6. use Date::Parse;
  7. use File::Touch;
  8. use File::Temp qw/ :mktemp /;
  9. use File::Basename;
  10. use FindBin qw($Bin);
  11. use lib $Bin;
  12. use Fetch;
  13. # Prototypes
  14. sub session($);
  15. sub fetch($);
  16. sub getSSE($);
  17. sub getDest($$$$);
  18. sub delTor($);
  19. sub guessExt($);
  20. sub processFile($$);
  21. sub unrar($$);
  22. sub seriesCleanupLocal($);
  23. sub seriesCleanupCore($);
  24. sub seriesCleanup($);
  25. sub readDir($$);
  26. sub findMediaFiles($$);
  27. sub runAndCheck(@);
  28. # Parameters
  29. my $maxAge = 2.5 * 86400;
  30. my $tvDir = `~/bin/video/mediaPath` . '/TV';
  31. my $monitoredExec = '/Users/profplump/bin/video/torrentMonitored.pl';
  32. my $host = 'http://localhost:9091';
  33. my $url = $host . '/transmission/rpc';
  34. my $content = '{"method":"torrent-get","arguments":{"fields":["hashString","id","addedDate","comment","creator","dateCreated","isPrivate","name","totalSize","pieceCount","pieceSize","downloadedEver","error","errorString","eta","haveUnchecked","haveValid","leftUntilDone","metadataPercentComplete","peersConnected","peersGettingFromUs","peersSendingToUs","rateDownload","rateUpload","recheckProgress","sizeWhenDone","status","trackerStats","uploadedEver","uploadRatio","seedRatioLimit","seedRatioMode","downloadDir","files","fileStats"]}}';
  35. my $delContent = '{"method":"torrent-remove","arguments":{"ids":["#_ID_#"], "delete-local-data":"true"}';
  36. my $delSleep = 10;
  37. my $RAR_MIN_FILES = 4;
  38. my $FIND_MAX_DEPTH = 5;
  39. # Debug
  40. my $DEBUG = 0;
  41. if ($ENV{'DEBUG'}) {
  42. $DEBUG = 1;
  43. }
  44. # Command-line parameters
  45. my ($force, $maxDays, $hash) = @ARGV;
  46. # If a hash is provided, always process matching torrents
  47. if (defined($hash)) {
  48. $force = 1;
  49. }
  50. # Init
  51. my $fetch = Fetch->new();
  52. # Check availability of web interface
  53. $fetch->url($host);
  54. if ($DEBUG) {
  55. print STDERR "Checking for torrent web interface\n";
  56. }
  57. $fetch->fetch('nocheck' => 1);
  58. if ($fetch->status_code() != 200) {
  59. if ($DEBUG) {
  60. die("Transmission web interface not available\n");
  61. }
  62. exit(0);
  63. }
  64. # Fetch the list of paused (i.e. completed) torrents
  65. $fetch->url($url);
  66. $fetch->post_content($content);
  67. &fetch($fetch);
  68. my $torrents = decode_json($fetch->content());
  69. $torrents = $torrents->{'arguments'}->{'torrents'};
  70. # Process
  71. if ($DEBUG) {
  72. print STDERR "Processing torrents...\n";
  73. }
  74. foreach my $tor (@{$torrents}) {
  75. # Skip torrents that aren't yet done
  76. if ($tor->{'leftUntilDone'} > 0 || $tor->{'metadataPercentComplete'} < 1) {
  77. next;
  78. }
  79. # Only process recent files
  80. # Torrents created with magnet links have no dateCreated, so fake it with addedDate
  81. if ($tor->{'dateCreated'} < 946684800) {
  82. $tor->{'dateCreated'} = $tor->{'addedDate'};
  83. }
  84. if (!$force && $tor->{'dateCreated'} < time() - $maxAge) {
  85. my $age = (time() - $tor->{'dateCreated'}) / 86400;
  86. if ($DEBUG) {
  87. print STDERR 'Skipping old torrent: ' . $tor->{'name'} . ' (' . sprintf('%.0f', $age) . " days)\n";
  88. }
  89. next;
  90. }
  91. # If a hash is provided, process only that file
  92. if (defined($hash) && $tor->{'hashString'} ne $hash) {
  93. if ($DEBUG) {
  94. printf STDERR 'Skipping torrent with non-matching hash: ' . $tor->{'name'} . ' (' . $tor->{'hashString'} . ")\n";
  95. }
  96. next;
  97. }
  98. # We've decided to do something about this torrent
  99. if ($DEBUG) {
  100. print STDERR 'Processing completed torrent: ' . $tor->{'name'} . "\n";
  101. }
  102. # Handle individual files and directories
  103. my @files = ();
  104. my @newFiles = ();
  105. my $path = $tor->{'downloadDir'} . '/' . $tor->{'name'};
  106. if (!-d $path) {
  107. push(@files, $path);
  108. } else {
  109. # State variables
  110. my $IS_RAR = '';
  111. my $IS_FAKE = 0;
  112. # Look for RAR files
  113. foreach my $file (@{ $tor->{'files'} }) {
  114. if ($file->{'name'} =~ /\.rar$/i) {
  115. $IS_RAR = $tor->{'downloadDir'} . '/' . $file->{'name'};
  116. last;
  117. }
  118. }
  119. # Check for password files
  120. foreach my $file (@{ $tor->{'files'} }) {
  121. if ($file->{'name'} =~ /passw(?:or)?d/i) {
  122. print STDERR 'Password file detected in: ' . $tor->{'name'} . "\n";
  123. $IS_FAKE = 1;
  124. last;
  125. }
  126. }
  127. # Check for single-file RARs
  128. if ($IS_RAR && scalar(@{ $tor->{'files'} }) < $RAR_MIN_FILES) {
  129. print STDERR 'Single-file RAR detected in: ' . $tor->{'name'} . "\n";
  130. $IS_FAKE = 1;
  131. }
  132. # Delete fake torrents and bail
  133. if ($IS_FAKE) {
  134. if ($DEBUG) {
  135. print STDERR 'Deleting fake torrent: ' . $tor->{'name'} . "\n";
  136. }
  137. delTor($tor);
  138. next;
  139. }
  140. # Unrar if necessary
  141. if (defined($IS_RAR) && length($IS_RAR) > 0) {
  142. @newFiles = &unrar($IS_RAR, $path);
  143. }
  144. # Find all media files, recursively
  145. @files = findMediaFiles($path, 0);
  146. }
  147. # Bail if we found no files
  148. if (scalar(@files) < 1) {
  149. print STDERR 'No media files found in torrent: ' . $tor->{'name'} . "\n";
  150. next;
  151. }
  152. # Do all the normal file naming/copying/etc. if we found at least one file
  153. my $failure = 0;
  154. foreach my $file (@files) {
  155. my $result = -1;
  156. if (defined($file) && length($file) > 0 && -r $file) {
  157. $result = &processFile($file, $path);
  158. }
  159. if (defined($result) && $result == 1) {
  160. if ($DEBUG) {
  161. print STDERR 'Torrent stored successfully: ' . $tor->{'name'} . "\n";
  162. }
  163. } elsif (defined($result) && $result == -1) {
  164. print STDERR 'Deleting bad torrent: ' . $tor->{'name'} . "\n";
  165. last;
  166. } else {
  167. print STDERR 'Error storing file "' . basename($file) . '" from torrent: ' . $tor->{'name'} . "\n";
  168. $failure = 1;
  169. last;
  170. }
  171. }
  172. # Delete any files we added
  173. foreach my $file (@newFiles) {
  174. if ($DEBUG) {
  175. print STDERR 'Deleting new file: ' . basename($file) . "\n";
  176. }
  177. unlink($file);
  178. }
  179. # Only delete the torrent on success
  180. if (!$failure) {
  181. delTor($tor);
  182. next;
  183. }
  184. }
  185. # Cleanup
  186. exit(0);
  187. # Grab the session ID
  188. sub session($) {
  189. my ($fetch) = @_;
  190. my %headers = ();
  191. ($headers{'X-Transmission-Session-Id'}) = $fetch->content() =~ /X\-Transmission\-Session\-Id\:\s+(\w+)/;
  192. $fetch->headers(\%headers);
  193. }
  194. sub fetch($) {
  195. my ($fetch) = @_;
  196. $fetch->fetch('nocheck' => 1);
  197. if ($fetch->status_code() != 200) {
  198. &session($fetch);
  199. $fetch->fetch();
  200. if ($fetch->status_code() != 200) {
  201. die('Unable to fetch: ' . $fetch->status_code() . "\n");
  202. }
  203. }
  204. }
  205. sub getSSE($) {
  206. my ($name) = @_;
  207. if ($DEBUG) {
  208. print STDERR 'Finding series/season/episode for: ' . $name . "\n";
  209. }
  210. my $season = 0;
  211. my $episode = 0;
  212. my $seasonBlock = '';
  213. if ($name =~ /(?:\b|_)(S(?:eason)?[_\s\.\-]*\d{1,2}[_\s\.\-]*E(?:pisode)?[_\s\.]*\d{1,2})(?:\b|_)/i) {
  214. $seasonBlock = $1;
  215. ($season, $episode) = $seasonBlock =~ /S(?:eason)?[_\s\.\-]*(\d{1,2})[_\s\.\-]*E(?:pisode)?[_\s\.\-]*(\d{1,2})/i;
  216. $season = int($season);
  217. $episode = sprintf('%02d', int($episode));
  218. } elsif ($name =~ /[\[\_\.](\d{1,2}x\d{2,3})[\]\_\.]/i) {
  219. $seasonBlock = $1;
  220. ($season, $episode) = $seasonBlock =~ /(\d+)x(\d+)/i;
  221. $season = int($season);
  222. $episode = sprintf('%02d', int($episode));
  223. } elsif ($name =~ /(?:\b|_)(20\d\d(?:\.|\-)[01]?\d(?:\.|\-)[0-3]?\d)(?:\b|_)/) {
  224. $seasonBlock = $1;
  225. my ($month, $day);
  226. ($season, $month, $day) = $seasonBlock =~ /(20\d\d)(?:\.|\-)([01]?\d)(?:\.|\-)([0-3]?\d)/;
  227. $season = int($season);
  228. $episode = sprintf('%04d-%02d-%02d', $season, $month, $day);
  229. } elsif ($name =~ /(?:\b|_)([01]?\d[_\s\.]?[0-3]\d)(?:\b|_)/i) {
  230. $seasonBlock = $1;
  231. ($season, $episode) = $seasonBlock =~ /(?:\b|_)([01]?\d)[_\s\.]?([0-3]\d)/i;
  232. $season = int($season);
  233. $episode = sprintf('%02d', int($episode));
  234. }
  235. if (!defined($seasonBlock) || $season < 1 || length($episode) < 1 || $episode eq '0') {
  236. if ($DEBUG) {
  237. print STDERR 'Could not find seasonBlock in: ' . $name . "\n";
  238. }
  239. return;
  240. }
  241. # Assume the series titles comes before the season/episode block
  242. my $series = '';
  243. my $sIndex = index($name, $seasonBlock);
  244. if ($sIndex > 0) {
  245. $series = substr($name, 0, $sIndex - 1);
  246. $series = seriesCleanupCore($series);
  247. }
  248. if ($DEBUG) {
  249. print STDERR 'Series: ' . $series . ' Season: ' . $season . ' Episode: ' . $episode . "\n";
  250. }
  251. return ($series, $season, $episode);
  252. }
  253. sub getDest($$$$) {
  254. my ($series, $season, $episode, $ext) = @_;
  255. if ($DEBUG) {
  256. print STDERR 'Finding destintion for: ' . $series . ' S' . $season . 'E' . $episode . "\n";
  257. }
  258. # Input cleanup
  259. $episode =~ s/^0+//;
  260. # Find all our existing TV series
  261. my @shows = ();
  262. my %showsCan = ();
  263. open(SHOWS, '-|', $monitoredExec, 'STORE')
  264. or die("Unable to fork: ${!}\n");
  265. while (<SHOWS>) {
  266. chomp;
  267. my $orig = $_;
  268. my $clean = seriesCleanupLocal($_);
  269. push(@shows, $clean);
  270. $showsCan{$clean} = $orig;
  271. }
  272. close SHOWS or die("Unable to read monitored series list: ${!} ${?}\n");
  273. # See if we can find a unique series name match
  274. my $sClean = seriesCleanup($series);
  275. my $sCleanCore = seriesCleanupCore($series);
  276. my $sMatch = '\b' . $sClean . '\b';
  277. $series = '';
  278. foreach my $show (@shows) {
  279. # Match the folder name or the search_name
  280. my $coarse_match = 0;
  281. if ($show =~ /${sMatch}/i) {
  282. $coarse_match = 1;
  283. } else {
  284. my $search_name = '';
  285. my $file = $tvDir . '/' . $showsCan{$show} . '/search_name';
  286. if (-e $file) {
  287. open(FH, $file)
  288. or die('Unable to read search_name file: ' . $file . "\n");
  289. $search_name = <FH>;
  290. close(FH);
  291. }
  292. if ($search_name) {
  293. $search_name = seriesCleanup($search_name);
  294. if ($search_name =~ /${sMatch}/i) {
  295. $coarse_match = 1;
  296. }
  297. }
  298. }
  299. # If we got a course match, continue checking
  300. if ($coarse_match) {
  301. # Enforce any secondary matching rules (for ambiguous titles)
  302. my $detail_match = 1;
  303. {
  304. my @lines;
  305. my $file = $tvDir . '/' . $showsCan{$show} . '/must_match';
  306. if (-e $file) {
  307. local ($/, *FH);
  308. open(FH, $file)
  309. or die('Unable to read must_match file: ' . $file . "\n");
  310. my @tmp = <FH>;
  311. close(FH);
  312. push(@lines, @tmp);
  313. }
  314. foreach my $line (@lines) {
  315. chomp($line);
  316. if (!eval('$sCleanCore =~ m' . $line)) {
  317. if ($DEBUG) {
  318. print STDERR 'Skipping ' . $show . ' due to must_match failure for: ' . $line . "\n";
  319. }
  320. $detail_match = 0;
  321. last;
  322. }
  323. }
  324. }
  325. if (!$detail_match) {
  326. next;
  327. }
  328. # If we find more than one matching series
  329. if (length($series) > 0) {
  330. # If the old match is a subset of the new match, prefer the new match
  331. # If the new match is a subet of the old match, prefer the old match
  332. # Bail if the matches do not overlap
  333. if ($show =~ m/^${series}\b/) {
  334. if ($DEBUG) {
  335. print STDERR 'Using longer match (' . $showsCan{$show} . ') in place of shorter match (' . $showsCan{$series} . ")\n";
  336. }
  337. $series = $show;
  338. } elsif ($series =~ m/^${show}\b/) {
  339. if ($DEBUG) {
  340. print STDERR 'Using longer match (' . $showsCan{$series} . ') in place of shorter match (' . $showsCan{$show} . ")\n";
  341. }
  342. $series = $series;
  343. } else {
  344. print STDERR 'Matched both ' . $showsCan{$series} . ' and ' . $showsCan{$show} . "\n";
  345. return;
  346. }
  347. } else {
  348. # If we're still around this is the first series title match
  349. $series = $show;
  350. }
  351. }
  352. }
  353. if (length($series) < 1) {
  354. if ($DEBUG) {
  355. print STDERR 'No series match for: ' . $sMatch . "\n";
  356. }
  357. return;
  358. }
  359. # Lookup the canonical name from the matched name
  360. $series = $showsCan{$series};
  361. # Make sure we have the right season folder
  362. my $seriesDir = $tvDir . '/' . $series;
  363. my $seasonDir = $seriesDir . '/' . 'Season ' . $season;
  364. if (!-d $seasonDir) {
  365. print STDERR 'No season folder for: ' . $series . ' S' . $season . "\n";
  366. return;
  367. }
  368. # Bail if we already have this episode
  369. my @episodes = readDir($seasonDir, 0);
  370. foreach my $ep (@episodes) {
  371. my ($epNum) = $ep =~ /^\s*([\d\-]+)\s*\-\s*/;
  372. if (!defined($epNum)) {
  373. next;
  374. }
  375. $epNum =~ s/^0+//;
  376. if ($epNum eq $episode) {
  377. print STDERR 'Existing episode for: ' . $series . ' S' . $season . 'E' . $episode . "\n";
  378. return;
  379. }
  380. }
  381. # Construct the final path
  382. if (!defined($ext) || length($ext) < 1) {
  383. $ext = 'avi';
  384. }
  385. if ($episode =~ /^\d+$/) {
  386. $episode = sprintf('%02d', $episode);
  387. }
  388. my $dest = sprintf('%s/%s - NoName.%s', $seasonDir, $episode, $ext);
  389. if ($DEBUG) {
  390. print STDERR 'Destination: ' . $dest . "\n";
  391. }
  392. return $dest;
  393. }
  394. sub delTor($) {
  395. my ($tor) = @_;
  396. if ($DEBUG) {
  397. print STDERR 'Deleting torrent: ' . $tor->{'name'} . "\n";
  398. }
  399. # Sleep to avoid bugs in Transmission (i.e. multiple deletes fail)
  400. sleep($delSleep);
  401. # Construct the content
  402. my $id = $tor->{'hashString'};
  403. my $content = $delContent;
  404. $content =~ s/\#_ID_\#/${id}/;
  405. # Send the command
  406. &session($fetch);
  407. $fetch->post_content($content);
  408. &fetch($fetch);
  409. }
  410. sub guessExt($) {
  411. my ($file) = @_;
  412. # Believe most non-avi file extensions without checking
  413. # It's mostly "avi" that lies, and the checks are expensive
  414. my $ext = '';
  415. {
  416. my $orig_ext = basename($file);
  417. $orig_ext =~ s/^.*\.(\w{2,3})$/$1/;
  418. $orig_ext = lc($orig_ext);
  419. if ($orig_ext eq 'mkv' || $orig_ext eq 'ts') {
  420. $ext = $orig_ext;
  421. }
  422. }
  423. # Ask movInfo.pl about the demuxer
  424. if (!$ext) {
  425. my $demux = '';
  426. open(INFO, '-|', $ENV{'HOME'} . '/bin/video/movInfo.pl', $file, 'DEMUXER');
  427. while (<INFO>) {
  428. $demux .= $_;
  429. }
  430. close(INFO);
  431. $demux =~ s/^\s+//;
  432. $demux =~ s/\s+$//;
  433. # Grab a MIME type from file(1)
  434. my $mime = '';
  435. open(FILE, '-|', 'file', '-b', $file);
  436. while (<FILE>) {
  437. $mime .= $_;
  438. }
  439. close(FILE);
  440. $mime =~ s/^\s+//;
  441. $mime =~ s/\s+$//;
  442. $mime =~ s/\;.*$//;
  443. $mime =~ s/^video\///i;
  444. # Try to pick an extension we understand
  445. $ext = 'avi';
  446. if ($demux =~ /mkv/i || $mime =~ /Matroska/i) {
  447. $ext = 'mkv';
  448. } elsif ($demux =~ /asf/i || $mime =~ /\bASF\b/i) {
  449. $ext = 'wmv';
  450. } elsif ($mime =~ /\bZIP/i) {
  451. $ext = 'zip';
  452. } elsif ($mime =~ /\bAVI\b/i) {
  453. $ext = 'avi';
  454. } elsif ($demux =~ /mpegts/i) {
  455. $ext = 'ts';
  456. }
  457. }
  458. return $ext;
  459. }
  460. sub processFile($$) {
  461. my ($file, $path) = @_;
  462. if ($DEBUG) {
  463. print STDERR 'Attempting to process file: ' . basename($file) . "\n";
  464. }
  465. # Guess a file extension
  466. my $ext = &guessExt($file);
  467. # Delete WMV files -- mostly viruses
  468. if ($ext =~ /wmv/i) {
  469. print STDERR 'Declining to save WMV file: ' . basename($file) . "\n";
  470. return -1;
  471. }
  472. # Delete ZIP files -- mostly fake
  473. if ($ext =~ /zip/i) {
  474. print STDERR 'Declining to save ZIP file: ' . basename($file) . "\n";
  475. return -1;
  476. }
  477. # Allow multiple guesses at the series/season/episode
  478. my $dest = '';
  479. my $filename = basename($file);
  480. LOOP: {
  481. # Determine the series, season, and episode number
  482. my ($series, $season, $episode) = &getSSE($filename);
  483. if ( defined($series)
  484. && length($series) > 0
  485. && defined($season)
  486. && $season > 0
  487. && defined($episode)
  488. && length($episode) > 0
  489. && $episode ne '0')
  490. {
  491. # Find the proper destination for this torrent, if any
  492. $dest = &getDest($series, $season, $episode, $ext);
  493. }
  494. # Sanity/loop check
  495. if (!defined($dest) || length($dest) < 1) {
  496. # If there is no match but the torrent is a folder
  497. # retry the guess using the torrent path
  498. if ($file ne $path) {
  499. my $newName = basename($path);
  500. if ($newName ne $filename) {
  501. $filename = basename($path);
  502. redo LOOP;
  503. }
  504. }
  505. # Otherwise we just fail
  506. print STDERR 'No destination for: ' . basename($file) . "\n";
  507. return;
  508. }
  509. }
  510. # Copy with system tools
  511. my $tmp = mktemp($dest . '.XXXXXXXX');
  512. runAndCheck(('cp', $file, $tmp));
  513. rename($tmp, $dest);
  514. # Touch to avoid carrying dates set in the download process
  515. touch($dest);
  516. # Return success
  517. return 1;
  518. }
  519. sub unrar($$) {
  520. my ($file, $path) = @_;
  521. if ($DEBUG) {
  522. print STDERR 'UnRARing: ' . basename($file) . "\n";
  523. }
  524. # Keep a list of old files, so we can find/delete the output
  525. my @beforeFiles = readDir($path, 1);
  526. # Run the unrar utility
  527. my $pid = fork();
  528. if (!defined($pid)) {
  529. print STDERR "Unable to fork for RAR\n";
  530. return;
  531. } elsif ($pid == 0) {
  532. # Child
  533. close(STDOUT);
  534. close(STDERR);
  535. chdir($path);
  536. my @args = ('unrar', 'e', '-p-', '-y', $file);
  537. exec { $args[0] } @args;
  538. }
  539. # Wait for the child
  540. waitpid($pid, 0);
  541. # Compare the old file list to the new one
  542. my @newFiles = ();
  543. my @afterFiles = readDir($path, 1);
  544. foreach my $file (@afterFiles) {
  545. my $found = 0;
  546. foreach my $file2 (@beforeFiles) {
  547. if ($file eq $file2) {
  548. $found = 1;
  549. last;
  550. }
  551. }
  552. if (!$found) {
  553. push(@newFiles, $file);
  554. }
  555. }
  556. # Return the list of added files
  557. return @newFiles;
  558. }
  559. sub seriesCleanupLocal($) {
  560. my ($name) = @_;
  561. $name =~ s/\.//g;
  562. return seriesCleanupCore($name);
  563. }
  564. sub seriesCleanupCore($) {
  565. my ($name) = @_;
  566. $name =~ s/\b(?:and|\&)\b/ /ig;
  567. $name =~ s/^\s*The\b//ig;
  568. $name =~ s/\[[^\]]*\]//g;
  569. $name =~ s/\{[^\}]*\}//g;
  570. $name =~ s/[\(\)]//g;
  571. $name =~ s/[\'\"]//g;
  572. $name =~ s/[^\w\s]+/ /g;
  573. $name =~ s/_+/ /g;
  574. $name =~ s/\s+/ /g;
  575. $name =~ s/^\s*//;
  576. $name =~ s/\s*$//;
  577. return $name;
  578. }
  579. sub seriesCleanup($) {
  580. my ($name) = @_;
  581. $name =~ s/\s*\(?US\)?\s*$//i;
  582. $name =~ s/\s*\(?20[01][0-9]\)?\s*$//;
  583. return seriesCleanupCore($name);
  584. }
  585. sub readDir($$) {
  586. my ($indir, $long) = @_;
  587. # Clean up the input directory
  588. $indir =~ s/\/+$//;
  589. my @files = ();
  590. if (!opendir(DIR, $indir)) {
  591. warn('readDir: Unable to read directory: ' . $indir . ': ' . $! . "\n");
  592. return;
  593. }
  594. foreach my $file (readdir(DIR)) {
  595. my $keep = 0;
  596. # Skip junk
  597. if ( $file eq '.'
  598. || $file eq '..'
  599. || $file =~ /\._/)
  600. {
  601. next;
  602. }
  603. # Save the full path
  604. if ($long) {
  605. push(@files, $indir . '/' . $file);
  606. } else {
  607. push(@files, $file);
  608. }
  609. }
  610. closedir(DIR);
  611. # Sorts the files before returning to the caller
  612. my @orderedFiles = ();
  613. @orderedFiles = sort(@files);
  614. # Return the file list
  615. return (@orderedFiles);
  616. }
  617. sub findMediaFiles($$) {
  618. my ($path, $depth) = @_;
  619. my @files = ();
  620. # Recursion limit
  621. if ($depth > $FIND_MAX_DEPTH) {
  622. return ();
  623. }
  624. my @tmpFiles = readDir($path, 1);
  625. foreach my $file (@tmpFiles) {
  626. # Skip sample files
  627. if ($file =~ /sample/i) {
  628. next;
  629. }
  630. # Recurse on directories
  631. if (-d $file) {
  632. push(@files, findMediaFiles($file, $depth + 1));
  633. next;
  634. }
  635. # Save matching files
  636. if ($file =~ /\.(?:avi|mkv|m4v|mov|mp4|ts|wmv)$/i) {
  637. push(@files, $file);
  638. next;
  639. }
  640. }
  641. return @files;
  642. }
  643. sub runAndCheck(@) {
  644. my (@args) = (@_);
  645. # Run
  646. system { $args[0] } @args;
  647. my $res = $?;
  648. # Die on any error
  649. if ($res == -1) {
  650. die('Unable to execute program: ' . join(' ', @args) . ': ' . $! . "\n");
  651. } elsif ($res & 127) {
  652. die('Child exited on signal: ' . join(' ', @args) . ': ' . ($res & 127) . "\n");
  653. } elsif ($res != 0) {
  654. die('Child exited with non-zero value: ' . join(' ', @args) . ': ' . ($res >> 8) . "\n");
  655. }
  656. # If we get here, all is well
  657. return 1;
  658. }