findTorrent.pl 16 KB

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