findTorrent.pl 18 KB

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