transmission.pl 17 KB

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