findTorrent.pl 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829
  1. #!/usr/bin/perl
  2. use strict;
  3. use warnings;
  4. # Defaults
  5. my $DEBUG = 0;
  6. my $NO_QUALITY_CHECKS = 0;
  7. my $MORE_NUMBER_FORMATS = 0;
  8. my $MIN_DAYS_BACK = 0;
  9. my $MAX_DAYS_BACK = 3;
  10. # Local config
  11. my $TV_DIR = `~/bin/video/mediaPath` . '/TV';
  12. # Search parameters
  13. my $PROTOCOL = 'https';
  14. my %ENABLE_SOURCE = ('TPB' => 1, 'ISO' => 0);
  15. # Selection parameters
  16. my $MIN_COUNT = 10;
  17. my $MIN_SIZE = 100;
  18. my $SIZE_BONUS = 7;
  19. my $SIZE_PENALTY = $SIZE_BONUS;
  20. my $MAX_SEED_RATIO = .25;
  21. my $SEED_RATIO_COUNT = 10;
  22. # App config
  23. my $DELAY = 4;
  24. my $TIMEOUT = 15;
  25. my $UA = 'Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_5_5; en-us) AppleWebKit/525.18 (KHTML, like Gecko) Version/3.1.2 Safari/525.20.1';
  26. # Includes
  27. use JSON;
  28. use File::Temp;
  29. use File::Spec;
  30. use File::Basename;
  31. use FindBin qw($Bin);
  32. use lib $Bin;
  33. use Fetch;
  34. # Prototypes
  35. sub findSE($);
  36. # Command line
  37. my ($dir, $search) = @ARGV;
  38. if (!defined($dir)) {
  39. die('Usage: ' . basename($0) . " input_directory [search_string]\n");
  40. }
  41. if ($ENV{'DEBUG'}) {
  42. $DEBUG = 1;
  43. }
  44. if ($ENV{'NO_QUALITY_CHECKS'}) {
  45. $NO_QUALITY_CHECKS = 1;
  46. }
  47. if ($ENV{'MORE_NUMBER_FORMATS'}) {
  48. $MORE_NUMBER_FORMATS = 1;
  49. }
  50. if ($ENV{'MIN_DAYS_BACK'}) {
  51. $MIN_DAYS_BACK = $ENV{'MIN_DAYS_BACK'};
  52. }
  53. if ($ENV{'MAX_DAYS_BACK'}) {
  54. $MAX_DAYS_BACK = $ENV{'MAX_DAYS_BACK'};
  55. }
  56. # New fetch object
  57. my $cookies = mktemp('/tmp/findTorrent.cookies.XXXXXXXX');
  58. my $fetch = Fetch->new(
  59. 'cookiefile' => $cookies,
  60. 'timeout' => $TIMEOUT,
  61. 'uas' => $UA
  62. );
  63. # Search sources
  64. my %SOURCES = ();
  65. # The Pirate Bay
  66. if ($ENABLE_SOURCE{'TPB'}) {
  67. # Available TPB proxies, in order of preference
  68. my @TPBs = ('thepiratebay.sx/search/', 'tpb.jasohack.com/index.php?loadurl=/search/', 'tpb.allestec.com/index.php?loadurl=/search/');
  69. # Automatically select a TPB proxy that returns a search page
  70. my $search_url = '';
  71. foreach my $url (@TPBs) {
  72. my ($host) = $url =~ /^([^\/]+)/;
  73. $fetch->url($PROTOCOL . '://' . $host);
  74. if ($fetch->fetch('nocheck' => 1) == 200 && $fetch->content() =~ /\bSearch\b/i) {
  75. $search_url = $url;
  76. last;
  77. } elsif ($DEBUG) {
  78. print STDERR 'TPB proxy not available: ' . $host . "\n";
  79. }
  80. }
  81. # Only add TPB if one of the proxies is up
  82. if ($search_url) {
  83. my %tmp = (
  84. 'search_url' => $search_url,
  85. 'search_suffix' => '/0/7/0',
  86. 'weight' => 1.5,
  87. 'quote' => '0'
  88. );
  89. $SOURCES{'TPB'} = \%tmp;
  90. }
  91. }
  92. # ISOhunt
  93. if ($ENABLE_SOURCE{'ISO'}) {
  94. # Available ISOhunt proxies, in order of preference
  95. my @ISOs = ('isohunt.com/js/json.php?rows=10&start=1&ihq=');
  96. # Automatically select a TPB proxy that returns the non-US page
  97. my $search_url = '';
  98. foreach my $url (@ISOs) {
  99. my ($host) = $url =~ /^([^\/]+)/;
  100. $fetch->url($PROTOCOL . '://' . $host);
  101. if ($fetch->fetch('nocheck' => 1) == 200 && $fetch->content() =~ /Last\s+\d+\s+files\s+indexed/i) {
  102. $search_url = $url;
  103. last;
  104. } elsif ($DEBUG) {
  105. print STDERR 'ISOhunt proxy not available: ' . $host . "\n";
  106. }
  107. }
  108. # Only add ISOhunt if one of the proxies is up
  109. if ($search_url) {
  110. my %tmp = (
  111. 'search_url' => 'isohunt.com/js/json.php?rows=10&start=1&ihq=',
  112. 'search_suffix' => '',
  113. 'weight' => 0.30,
  114. 'quote' => 1
  115. );
  116. $SOURCES{'ISO'} = \%tmp;
  117. }
  118. }
  119. # Sanity check
  120. if (scalar(keys(%SOURCES)) < 1) {
  121. die("No sources available\n");
  122. }
  123. # Environment
  124. #if ($DEBUG) {
  125. # print STDERR `printenv` . "\n";
  126. #}
  127. # Figure out what we're searching for
  128. my $show = '';
  129. my @urls = ();
  130. my $CUSTOM_SEARCH = 0;
  131. my $season = 0;
  132. my @need = ();
  133. my %need = ();
  134. # Clean up the input "directory" path
  135. {
  136. # Allow use of the raw series name
  137. if (!($dir =~ /\//)) {
  138. $show = $dir;
  139. $dir = $TV_DIR . '/' . $dir;
  140. }
  141. # Allow use of relative paths
  142. $dir = File::Spec->rel2abs($dir);
  143. # Sanity check
  144. if (!-d $dir) {
  145. die('Invalid input directory: ' . $dir . "\n");
  146. }
  147. # Isolate the season from the path, if provided
  148. $dir =~ /\/Season\s+(\d+)\/?$/i;
  149. if ($1) {
  150. $season = $1;
  151. $dir = dirname($dir);
  152. }
  153. # If no season is provided find the latest
  154. if (!$season) {
  155. opendir(SERIES, $dir)
  156. or die("Unable to open series directory: ${!}\n");
  157. while (my $file = readdir(SERIES)) {
  158. if ($file =~ /^Season\s+(\d+)$/i) {
  159. if (!$season || $season < $1) {
  160. $season = $1;
  161. }
  162. }
  163. }
  164. closedir(SERIES);
  165. }
  166. # Isolate and clean the series name
  167. if (!$show) {
  168. $show = basename($dir);
  169. }
  170. $show =~ s/[\'\"\.]//g;
  171. if ($DEBUG) {
  172. print STDERR 'Checking directory: ' . $dir . "\n";
  173. }
  174. }
  175. # Allow quality checks to be disabled
  176. if (-e $dir . '/no_quality_checks') {
  177. $NO_QUALITY_CHECKS = 1;
  178. }
  179. # Handle custom searches
  180. if ((scalar(@urls) < 1) && defined($search) && length($search) > 0) {
  181. # Note the custom search string
  182. $CUSTOM_SEARCH = 1;
  183. if ($DEBUG) {
  184. print STDERR "Custom search\n";
  185. }
  186. # Create the relevent search strings
  187. foreach my $key (keys(%SOURCES)) {
  188. my $source = $SOURCES{$key};
  189. push(@urls, $PROTOCOL . '://' . $source->{'search_url'} . $search . $source->{'search_suffix'});
  190. }
  191. }
  192. # Handle search-by-date series
  193. if ((scalar(@urls) < 1) && -e $dir . '/search_by_date') {
  194. # Note the search-by-date status
  195. # Set the CUSTOM_SERACH flag to skip season/epsiode matching
  196. $CUSTOM_SEARCH = 1;
  197. if ($DEBUG) {
  198. print STDERR "Find by date\n";
  199. }
  200. # Read the find-by-date string
  201. my $search_by_date = '';
  202. local ($/, *FH);
  203. open(FH, $dir . '/search_by_date')
  204. or die('Unable to read search_by_date for show: ' . $show . ': ' . $! . "\n");
  205. my $text = <FH>;
  206. close(FH);
  207. if ($text =~ /^\s*(\S.*\S)\s*$/) {
  208. $search_by_date = $1;
  209. } else {
  210. die('Skipping invalid search_by_date for show: ' . $show . ': ' . $text . "\n");
  211. }
  212. # Create search strings for each date in the range, unless the related file already exists
  213. for (my $days_back = $MIN_DAYS_BACK ; $days_back <= $MAX_DAYS_BACK ; $days_back++) {
  214. # Calculate the date
  215. my (undef(), undef(), undef(), $day, $month, $year) = localtime(time() - (86400 * $days_back));
  216. # Format as strings
  217. $year = sprintf('%04d', $year + 1900);
  218. $month = sprintf('%02d', $month + 1);
  219. $day = sprintf('%02d', $day);
  220. # Check for an existing file
  221. my $exists = 0;
  222. {
  223. my $season_dir = $dir . '/Season ' . $year;
  224. my $prefix = qr/${year}\-${month}\-${day}\s*\-\s*/;
  225. opendir(SEASON, $season_dir)
  226. or die("Unable to open season directory: ${!}\n");
  227. while (my $file = readdir(SEASON)) {
  228. if ($file =~ $prefix) {
  229. $exists = 1;
  230. last;
  231. }
  232. }
  233. closedir(SEASON);
  234. }
  235. if ($exists) {
  236. next;
  237. }
  238. # Create the relevent search strings
  239. my $search_str = $search_by_date;
  240. $search_str =~ s/%Y/${year}/g;
  241. $search_str =~ s/%m/${month}/g;
  242. $search_str =~ s/%d/${day}/g;
  243. foreach my $key (keys(%SOURCES)) {
  244. my $source = $SOURCES{$key};
  245. push(@urls, $PROTOCOL . '://' . $source->{'search_url'} . $search_str . $source->{'search_suffix'});
  246. }
  247. }
  248. }
  249. # Handle standard series
  250. if (scalar(@urls) < 1) {
  251. # Allow use of more number formats
  252. if (-e $dir . '/more_number_formats') {
  253. $MORE_NUMBER_FORMATS = 1;
  254. }
  255. # Allow the show name to be overriden
  256. {
  257. my $search_name = $dir . '/search_name';
  258. if (-e $search_name) {
  259. local ($/, *FH);
  260. open(FH, $search_name)
  261. or die('Unable to read search_name for show: ' . $show . ': ' . $! . "\n");
  262. my $text = <FH>;
  263. close(FH);
  264. if ($text =~ /^\s*(\S.*\S)\s*$/) {
  265. $show = $1;
  266. } else {
  267. print STDERR 'Skipping invalid search_name for show: ' . $show . ': ' . $text . "\n";
  268. }
  269. }
  270. }
  271. if ($DEBUG) {
  272. print STDERR 'Searching with series title: ' . $show . "\n";
  273. }
  274. # Validate the season number
  275. if (!defined($season) || $season < 1 || $season > 2000) {
  276. die('Invalid season number: ' . $show . ' => ' . $season . "\n");
  277. }
  278. # Get the last episode number
  279. my $no_next = 0;
  280. my %episodes = ();
  281. my $highest = 0;
  282. opendir(SEASON, $dir . '/Season ' . $season)
  283. or die("Unable to open season directory: ${!}\n");
  284. while (my $file = readdir(SEASON)) {
  285. # Skip support files
  286. if ($file =~ /\.(?:png|xml|jpg|gif|tbn|txt|nfo|torrent)\s*$/i) {
  287. next;
  288. }
  289. # Check for a season_done file
  290. if ($file eq 'season_done') {
  291. $no_next = 1;
  292. next;
  293. }
  294. # Extract the episode number
  295. my ($num) = $file =~ /^\s*\.?(\d+)\s*\-\s*/i;
  296. if (defined($num)) {
  297. $num = int($num);
  298. if ($DEBUG) {
  299. print STDERR 'Found episode number: ' . $num . ' in file: ' . $file . "\n";
  300. }
  301. # Record it
  302. $episodes{$num} = 1;
  303. # Track the highest episode number
  304. if ($num > $highest) {
  305. $highest = $num;
  306. }
  307. }
  308. }
  309. close(SEASON);
  310. # Assume we need the next 2 episodes, unless no_next is set (i.e. season_done)
  311. if (!$no_next) {
  312. @need = ($highest + 1, $highest + 2);
  313. }
  314. # Find any missing episodes
  315. for (my $i = 1 ; $i <= $highest ; $i++) {
  316. if (!$episodes{$i}) {
  317. push(@need, $i);
  318. }
  319. }
  320. if ($DEBUG) {
  321. print STDERR 'Needed episodes: ' . join(', ', @need) . "\n";
  322. }
  323. # Reverse the array for later matching
  324. foreach my $episode (@need) {
  325. $need{$episode} = 1;
  326. }
  327. # Check for URL files
  328. if (-e "${dir}/url") {
  329. open(URL, "${dir}/url")
  330. or die("Unable to open URLs file: ${!}\n");
  331. while (<URL>) {
  332. if (/^\s*[a-z]+(\:\S+)\s*$/) {
  333. push(@urls, $PROTOCOL . $1);
  334. }
  335. }
  336. close(URL);
  337. }
  338. # Construct a URL-friendly show name
  339. my $safeShow = $show;
  340. $safeShow =~ s/\s+\&\s+/ and /i;
  341. $safeShow =~ s/^\s*The\b//i;
  342. $safeShow =~ s/\'//g;
  343. $safeShow =~ s/[^\w\"\-]+/ /g;
  344. $safeShow =~ s/^\s+//;
  345. $safeShow =~ s/\s+$//;
  346. $safeShow =~ s/([^A-Za-z0-9])/sprintf("%%%02X", ord($1))/seg;
  347. $safeShow =~ s/\%20/\+/g;
  348. # Calculate possible name variations
  349. my @urlShowVarients = ();
  350. {
  351. # Default name
  352. push(@urlShowVarients, $safeShow);
  353. # Search for both "and" and "&"
  354. if ($safeShow =~ /\+and\+/i) {
  355. my $tmp = $safeShow;
  356. $tmp =~ s/\+and\+/\+%26\+/;
  357. push(@urlShowVarients, $tmp);
  358. }
  359. }
  360. # Construct the URL for each title varient of each needed episode
  361. foreach my $urlShow (@urlShowVarients) {
  362. foreach my $episode (@need) {
  363. my $episode_long = sprintf('%02d', $episode);
  364. my $season_long = sprintf('%02d', $season);
  365. foreach my $source (values(%SOURCES)) {
  366. my $quote = '%22';
  367. if (!$source->{'quote'}) {
  368. $quote = '';
  369. }
  370. # SXXEYY
  371. my $url = $PROTOCOL . '://' . $source->{'search_url'} . $quote . $urlShow . $quote . '+s' . $season_long . 'e' . $episode_long;
  372. if ($source->{'search_suffix'}) {
  373. $url .= $source->{'search_suffix'};
  374. }
  375. push(@urls, $url);
  376. # Extra searches for shows that have lazy/non-standard number formats
  377. if ($MORE_NUMBER_FORMATS) {
  378. # SXEY
  379. if ($season_long ne $season || $episode_long ne $episode) {
  380. $url = $PROTOCOL . '://' . $source->{'search_url'} . $quote . $urlShow . $quote . '+s' . $season . 'e' . $episode;
  381. if ($source->{'search_suffix'}) {
  382. $url .= $source->{'search_suffix'};
  383. }
  384. }
  385. push(@urls, $url);
  386. # SXX EYY
  387. $url = $PROTOCOL . '://' . $source->{'search_url'} . $quote . $urlShow . $quote . '+s' . $season_long . '+e' . $episode_long;
  388. if ($source->{'search_suffix'}) {
  389. $url .= $source->{'search_suffix'};
  390. }
  391. push(@urls, $url);
  392. # Season XX Episode YY
  393. $url = $PROTOCOL . '://' . $source->{'search_url'} . $quote . $urlShow . $quote . '+season+' . $season_long . '+episode+' . $episode_long;
  394. if ($source->{'search_suffix'}) {
  395. $url .= $source->{'search_suffix'};
  396. }
  397. push(@urls, $url);
  398. # Series X Episode Y
  399. $url = $PROTOCOL . '://' . $source->{'search_url'} . $quote . $urlShow . $quote . '+series+' . $season . '+episode+' . $episode;
  400. if ($source->{'search_suffix'}) {
  401. $url .= $source->{'search_suffix'};
  402. }
  403. push(@urls, $url);
  404. # SxEE
  405. $url = $PROTOCOL . '://' . $source->{'search_url'} . $quote . $urlShow . $quote . '+' . $season . 'x' . $episode_long;
  406. if ($source->{'search_suffix'}) {
  407. $url .= $source->{'search_suffix'};
  408. }
  409. push(@urls, $url);
  410. }
  411. }
  412. }
  413. }
  414. }
  415. my $content = '';
  416. my @json_content = ();
  417. foreach my $url (@urls) {
  418. # Fetch the page
  419. if ($DEBUG) {
  420. print STDERR 'Searching with URL: ' . $url . "\n";
  421. $fetch->file('/tmp/findTorrent-lastPage.html');
  422. }
  423. sleep($DELAY * (rand(2) + 0.5));
  424. $fetch->url($url);
  425. $fetch->fetch();
  426. # Check for errors
  427. if ($fetch->status_code() != 200) {
  428. print STDERR 'Error fetching URL: ' . $url . "\n";
  429. }
  430. # Save and compact the content
  431. if ($DEBUG) {
  432. print STDERR 'Fetched ' . length($fetch->content()) . " bytes\n";
  433. }
  434. if ($fetch->content() =~ /^\s*{/) {
  435. my $json = eval { decode_json($fetch->content()); };
  436. if (defined($json) && ref($json)) {
  437. push(@json_content, \@{ $json->{'items'}->{'list'} });
  438. } elsif ($DEBUG) {
  439. print STDERR 'JSON parsing failure on: ' . $fetch->content() . "\n";
  440. }
  441. } else {
  442. $content .= ' ' . $fetch->content();
  443. $content =~ s/\s+/ /g;
  444. }
  445. }
  446. # Find each TR element in TPB content
  447. my @tors = ();
  448. my @trs = split(/\<tr(?:\s+[^\>]*)?\>/i, $content);
  449. if ($DEBUG) {
  450. print STDERR 'TR count: ' . scalar(@trs) . "\n";
  451. }
  452. foreach my $tr (@trs) {
  453. # Trim trailing tags
  454. if (!($tr =~ s/\<\/tr\>.*$//is)) {
  455. # Skip things that aren't compelete TRs
  456. if ($DEBUG > 1) {
  457. print STDERR 'Skipping non-TR line: ' . $tr . "\n\n";
  458. }
  459. next;
  460. }
  461. # Find the show title
  462. my ($title) = $tr =~ /title\=\"Details\s+for\s+([^\"]*)\"/i;
  463. if (!defined($title) || length($title) < 1) {
  464. if ($DEBUG) {
  465. print STDERR "Unable to find show title in TR\n";
  466. }
  467. next;
  468. }
  469. $title =~ s/^\s+//;
  470. # Extract the season and episode numbers
  471. my ($fileSeason, $episode) = findSE($title);
  472. # Extract the URL
  473. my ($url) = $tr =~ /\<a\s+href\=\"(magnet\:\?[^\"]+)\"/i;
  474. if (!defined($url) || length($url) < 1) {
  475. if ($DEBUG) {
  476. print STDERR "Skipping TR with no magnet URL\n";
  477. }
  478. next;
  479. }
  480. # Count the sum of seeders and leachers
  481. my $seeds = 0;
  482. my $leaches = 0;
  483. if ($tr =~ /\<td(?:\s+[^\>]*)?\>(\d+)\<\/td\>\s*\<td(?:\s+[^\>]*)?\>(\d+)\<\/td\>\s*$/i) {
  484. $seeds = $1;
  485. $leaches = $2;
  486. }
  487. # Extract the size (from separate column or inline)
  488. my $size = 0;
  489. my $unit = 'M';
  490. if ($tr =~ m/Size (\d+(?:\.\d+)?)\&nbsp\;(G|M)iB/) {
  491. $size = $1;
  492. $unit = $2;
  493. } elsif ($tr =~ m/(\d+(?:\.\d+)?)\&nbsp\;(G|M)iB\<\/[tT][dD]\>/) {
  494. $size = $1;
  495. $unit = $2;
  496. }
  497. if ($unit eq 'G') {
  498. $size *= 1024;
  499. }
  500. $size = int($size);
  501. if ($DEBUG) {
  502. print STDERR 'Found file (' . $title . '): ' . $url . "\n";
  503. }
  504. # Save the extracted data
  505. my %tor = (
  506. 'title' => $title,
  507. 'season' => $fileSeason,
  508. 'episode' => $episode,
  509. 'seeds' => $seeds,
  510. 'leaches' => $leaches,
  511. 'size' => $size,
  512. 'url' => $url,
  513. 'source' => 'TPB'
  514. );
  515. push(@tors, \%tor);
  516. }
  517. # Find each discrete torrent from ISOhunt
  518. foreach my $json (@json_content) {
  519. foreach my $tor (@{$json}) {
  520. # Extract the title
  521. my $title = $tor->{'title'};
  522. $title =~ s/\<\/?b\>//gi;
  523. $title =~ s/^\s+//;
  524. # Extract the season and episode numbers
  525. my ($fileSeason, $episode) = findSE($tor->{'title'});
  526. # Count the sum of seeders and leachers
  527. my $seeds = 0;
  528. my $leaches = 0;
  529. if (defined($tor->{'Seeds'}) && length($tor->{'Seeds'}) > 0) {
  530. $seeds = $tor->{'Seeds'};
  531. }
  532. if (defined($tor->{'leechers'}) && length($tor->{'leechers'}) > 0) {
  533. $leaches = $tor->{'leechers'};
  534. }
  535. # Extract the size
  536. my $size = 0;
  537. if (defined($tor->{'length'}) && length($tor->{'length'}) > 0) {
  538. $size = int($tor->{'length'} / 1048576);
  539. }
  540. if ($DEBUG) {
  541. print STDERR 'Found file (' . $title . '): ' . $tor->{'enclosure_url'} . "\n";
  542. }
  543. # Save the extracted data
  544. my %tmp = (
  545. 'title' => $title,
  546. 'season' => $fileSeason,
  547. 'episode' => $episode,
  548. 'seeds' => $seeds,
  549. 'leaches' => $leaches,
  550. 'size' => $size,
  551. 'url' => $tor->{'enclosure_url'},
  552. 'source' => 'ISO'
  553. );
  554. push(@tors, \%tmp);
  555. }
  556. }
  557. # Filter for size/count/etc.
  558. my %tors = ();
  559. my $showRegex = $show;
  560. $showRegex =~ s/[\"\']//g;
  561. $showRegex =~ s/[\W_]+/\[\\W_\].*/g;
  562. foreach my $tor (@tors) {
  563. # Skip files that don't start with our show title
  564. if (!($tor->{'title'} =~ /^${showRegex}[\W_]/i)) {
  565. if ($DEBUG) {
  566. print STDERR 'Skipping file: Title does not match (' . $showRegex . '): ' . $tor->{'title'} . "\n";
  567. }
  568. next;
  569. # Skip pre-air files
  570. } elsif ($tor->{'title'} =~ /preair/i) {
  571. if ($DEBUG) {
  572. print STDERR 'Skipping file: Title contains "preair": ' . $tor->{'title'} . "\n";
  573. }
  574. next;
  575. # Skip SWESUB files
  576. } elsif ($tor->{'title'} =~ /swesub/i) {
  577. if ($DEBUG) {
  578. print STDERR 'Skipping file: Title contains "SWESUB": ' . $tor->{'title'} . "\n";
  579. }
  580. next;
  581. # Skip .rus. files
  582. } elsif ($tor->{'title'} =~ /\.rus\./i) {
  583. if ($DEBUG) {
  584. print STDERR 'Skipping file: Title contains ".rus.": ' . $tor->{'title'} . "\n";
  585. }
  586. next;
  587. # Skip german files
  588. } elsif ($tor->{'title'} =~ /german/i) {
  589. if ($DEBUG) {
  590. print STDERR 'Skipping file: Title contains "german": ' . $tor->{'title'} . "\n";
  591. }
  592. next;
  593. }
  594. # Only apply season and episode number matches if CUSTOM_SEARCH is not in effect
  595. if (!$CUSTOM_SEARCH) {
  596. # Skip files that don't contain the right season number
  597. if (!defined($tor->{'season'}) || $tor->{'season'} != $season) {
  598. if ($DEBUG) {
  599. print STDERR 'Skipping file: No match for season number (' . $season . '): ' . $tor->{'title'} . "\n";
  600. }
  601. next;
  602. # Skip files that don't contain the episode number
  603. } elsif ((!defined($tor->{'episode'}) || !$need{ $tor->{'episode'} })) {
  604. if ($DEBUG) {
  605. print STDERR 'Skipping file: No match for episode number (' . $tor->{'episode'} . '): ' . $tor->{'title'} . "\n";
  606. }
  607. next;
  608. }
  609. }
  610. # Only apply the quality rules if NO_QUALITY_CHECKS is not in effect
  611. if (!$NO_QUALITY_CHECKS) {
  612. # Skip torrents with too few seeders/leachers
  613. if (($tor->{'seeds'} + $tor->{'leaches'}) * $SOURCES{ $tor->{'source'} }->{'weight'} < $MIN_COUNT) {
  614. if ($DEBUG) {
  615. print STDERR 'Skipping file: Insufficient seeder/leacher count (' . $tor->{'seeds'} . '/' . $tor->{'leaches'} . '): ' . $tor->{'title'} . "\n";
  616. }
  617. next;
  618. # Skip torrents with unusual seed/leach ratios
  619. } elsif ($tor->{'seeds'} > 1
  620. && $tor->{'seeds'} < $SEED_RATIO_COUNT
  621. && $tor->{'seeds'} > $tor->{'leaches'} * $MAX_SEED_RATIO)
  622. {
  623. if ($DEBUG) {
  624. print STDERR 'Skipping file: Unusual seeder/leacher ratio (' . $tor->{'seeds'} . '/' . $tor->{'leaches'} . '): ' . $tor->{'title'} . "\n";
  625. }
  626. next;
  627. # Skip torrents that are too small
  628. } elsif ($tor->{'size'} < $MIN_SIZE) {
  629. if ($DEBUG) {
  630. print STDERR 'Skipping file: Insufficient size (' . $tor->{'size'} . ' MiB): ' . $tor->{'title'} . "\n";
  631. }
  632. next;
  633. }
  634. }
  635. # Save good torrents
  636. push(@{ $tors{ $tor->{'episode'} } }, $tor);
  637. if ($DEBUG) {
  638. print STDERR 'Possible URL (' . $tor->{'seeds'} . '/' . $tor->{'leaches'} . ' seeds/leaches, ' . $tor->{'size'} . ' MiB): ' . $tor->{'title'} . "\n";
  639. }
  640. }
  641. # Find the average torrent size for each episode
  642. my %avg = ();
  643. foreach my $episode (keys(%tors)) {
  644. my $count = 0;
  645. $avg{$episode} = 0.001;
  646. foreach my $tor (@{ $tors{$episode} }) {
  647. $avg{$episode} += $tor->{'size'};
  648. $count++;
  649. }
  650. if ($count > 0) {
  651. $avg{$episode} /= $count;
  652. }
  653. if ($DEBUG) {
  654. print STDERR 'Episode ' . $episode . ' average size: ' . int($avg{$episode}) . " MiB\n";
  655. }
  656. }
  657. # Calculate an adjusted count based on the original count and the relative file size
  658. foreach my $episode (keys(%tors)) {
  659. foreach my $tor (@{ $tors{$episode} }) {
  660. my $count = $tor->{'seeds'} + $tor->{'leaches'};
  661. if ($tor->{'size'} >= $avg{$episode}) {
  662. $tor->{'adj_count'} = int($count * ($SIZE_BONUS * ($tor->{'size'} / $avg{$episode})));
  663. } else {
  664. $tor->{'adj_count'} = int($count * ((1 / $SIZE_PENALTY) * ($tor->{'size'} / $avg{$episode})));
  665. }
  666. if ($DEBUG) {
  667. print STDERR 'Possible URL (' . $tor->{'adj_count'} . ' size-adjusted sources): ' . $tor->{'url'} . "\n";
  668. }
  669. }
  670. }
  671. # Pick the best-adjusted-count torrent for each episode
  672. my %urls = ();
  673. foreach my $episode (keys(%tors)) {
  674. my $max = undef();
  675. foreach my $tor (@{ $tors{$episode} }) {
  676. if (!defined($max) || $tor->{'adj_count'} > $max) {
  677. $urls{$episode} = $tor->{'url'};
  678. $max = $tor->{'adj_count'};
  679. if ($DEBUG) {
  680. print STDERR 'New semi-final URL (adjusted count: ' . $tor->{'adj_count'} . '): ' . $tor->{'url'} . "\n";
  681. }
  682. } elsif ($DEBUG) {
  683. print STDERR 'Skipping for non-max adjusted count (' . $tor->{'adj_count'} . '): ' . $tor->{'url'} . "\n";
  684. }
  685. }
  686. }
  687. # Output
  688. foreach my $episode (keys(%tors)) {
  689. if (defined($urls{$episode})) {
  690. print $urls{$episode} . "\n";
  691. if ($DEBUG) {
  692. print STDERR 'Final URL: ' . $urls{$episode} . "\n";
  693. }
  694. } elsif ($DEBUG) {
  695. print STDERR 'No URL found for URL: ' . $episode . "\n";
  696. }
  697. }
  698. # Cleanup
  699. unlink($cookies);
  700. exit(0);
  701. # Extract season
  702. sub findSE($) {
  703. my ($title) = @_;
  704. my $season = 0;
  705. my $episode = 0;
  706. if ($title =~ /[\W_]s(?:eason|eries)?\s*(\d+)/i) {
  707. $season = $1;
  708. } elsif ($title =~ /\b(\d+)x(\d+)\b/i) {
  709. $season = $1;
  710. $episode = $2;
  711. }
  712. if (!$episode && $title =~ /e(?:ipsode)?\s*(\d{1,2})/i) {
  713. $episode = $1;
  714. }
  715. $season = int($season);
  716. $episode = int($episode);
  717. return ($season, $episode);
  718. }