findTorrent.pl 47 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918
  1. #!/usr/bin/perl
  2. use strict;
  3. use warnings;
  4. # Defaults
  5. my $DEBUG = 0;
  6. my $SYSLOG = 1;
  7. my $NO_QUALITY_CHECKS = 0;
  8. my $MORE_NUMBER_FORMATS = 0;
  9. my $MIN_DAYS_BACK = 0;
  10. my $MAX_DAYS_BACK = 3;
  11. my $NEXT_EPISODES = 3;
  12. # Local config
  13. my $TV_DIR = `~/bin/video/mediaPath` . '/TV';
  14. my $CONF_FILE = $ENV{'HOME'} . +'/.findTorrent.config';
  15. my $EXCLUDES_FILE = $ENV{'HOME'} . '/.findTorrent.exclude';
  16. # Search parameters
  17. my $PROTOCOL = 'https';
  18. my %ENABLE_SOURCE = ('NCAT' => 0, 'TPB' => 1, 'ISO' => 1, 'KICK' => 0, 'Z' => 0, 'ET' => 1);
  19. # Selection parameters
  20. my $MIN_COUNT = 10;
  21. my $MIN_SIZE = 100;
  22. my $SIZE_BONUS = 5;
  23. my $SIZE_PENALTY = $SIZE_BONUS;
  24. my $TITLE_PENALTY = $SIZE_BONUS / 2;
  25. my $MAX_SEED_RATIO = .25;
  26. my $SEED_RATIO_COUNT = 10;
  27. my $TRACKER_LOOKUP = 1;
  28. my $QUALITY_AGE = (86400 * 365);
  29. # Static tracker list, appended to all magnet URIs
  30. my @TRACKERS = (
  31. 'udp://open.demonii.com:1337/announce', 'udp://tracker.publicbt.com:80/announce',
  32. 'udp://tracker.openbittorrent.com:80/announce', 'udp://9.rarbg.com:2710/announce'
  33. );
  34. # App config
  35. my $SLEEP = 1;
  36. my $DELAY = 10;
  37. my $TIMEOUT = 15;
  38. my $ERR_DELAY = $TIMEOUT * 2;
  39. my $ERR_RETRIES = 3;
  40. my $COOKIES = undef();
  41. my $FETCH = undef();
  42. my $PHANTOM = '/usr/local/bin/phantomjs';
  43. my $UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10) AppleWebKit/538.39.41 (KHTML, like Gecko) Version/8.0 Safari/538.39.41';
  44. my $PHANTOM_CONFIG = "var system = require('system');
  45. var page = require('webpage').create();
  46. var resources = [];
  47. page.open(system.args[1], function(status)
  48. {
  49. console.log(resources[0].status);
  50. console.log(page.content);
  51. phantom.exit();
  52. });
  53. // Add responses for completed text/html resources
  54. page.onResourceReceived = function(response) {
  55. if (response.stage !== 'end') return;
  56. if (response.headers.filter(function(header) {
  57. if (header.name == 'Content-Type' && header.value.indexOf('text/html') == 0) {
  58. return true;
  59. }
  60. return false;
  61. }).length > 0)
  62. resources.push(response);
  63. };";
  64. # Includes
  65. use Date::Parse;
  66. use URI::Encode qw(uri_encode);
  67. use HTML::Entities;
  68. use HTML::Strip;
  69. use JSON;
  70. use Sys::Syslog qw(:standard :macros);
  71. use Capture::Tiny ':all';
  72. use File::Temp;
  73. use File::Spec;
  74. use File::Basename;
  75. use FindBin qw($Bin);
  76. use lib $Bin;
  77. use Fetch;
  78. # Prototypes
  79. sub getHash($);
  80. sub resolveSecondary($);
  81. sub resolveTrackers($);
  82. sub splitTags($$$$);
  83. sub findSE($);
  84. sub initSources();
  85. sub findProxy($$);
  86. sub fetch($;$$);
  87. sub phantomFetch($$);
  88. # Command line
  89. my ($dir, $search) = @ARGV;
  90. if (!defined($dir)) {
  91. die('Usage: ' . basename($0) . " input_directory [search_string]\n");
  92. }
  93. # Environment
  94. if ($ENV{'DEBUG'}) {
  95. if ($ENV{'DEBUG'} =~ /(\d+)/) {
  96. $DEBUG = $1;
  97. } else {
  98. $DEBUG = 1;
  99. }
  100. }
  101. if (exists($ENV{'EXCLUDES_FILE'})) {
  102. $EXCLUDES_FILE = $ENV{'EXCLUDES_FILE'};
  103. }
  104. if ($ENV{'NO_QUALITY_CHECKS'}) {
  105. $NO_QUALITY_CHECKS = 1;
  106. }
  107. if ($ENV{'MORE_NUMBER_FORMATS'}) {
  108. $MORE_NUMBER_FORMATS = 1;
  109. }
  110. if (defined($ENV{'MIN_DAYS_BACK'})) {
  111. $MIN_DAYS_BACK = $ENV{'MIN_DAYS_BACK'};
  112. }
  113. if (defined($ENV{'MAX_DAYS_BACK'})) {
  114. $MAX_DAYS_BACK = $ENV{'MAX_DAYS_BACK'};
  115. }
  116. if (defined($ENV{'NEXT_EPISODES'})) {
  117. $NEXT_EPISODES = $ENV{'NEXT_EPISODES'};
  118. }
  119. if (defined($ENV{'SYSLOG'})) {
  120. $SYSLOG = $ENV{'SYSLOG'};
  121. }
  122. # Read the config file
  123. my %CONFIG = ();
  124. if ($CONF_FILE && -r $CONF_FILE) {
  125. my $fh;
  126. open($fh, '<', $CONF_FILE)
  127. or die('Unable to open config file: ' . $CONF_FILE . ': ' . $! . "\n");
  128. while (<$fh>) {
  129. # Skip blank lines and comments
  130. if (/^\s*#/ || /^\s*$/) {
  131. next;
  132. }
  133. # Assume everything else is key = val pairs
  134. if (my ($key, $val) = $_ =~ /^\s*(\S[^\=]+)\=\>?\s*(\S.*)$/) {
  135. $key =~ s/^\s+//;
  136. $key =~ s/\s+$//;
  137. $val =~ s/^\s+//;
  138. $val =~ s/\s+$//;
  139. $CONFIG{$key} = $val;
  140. if ($DEBUG) {
  141. print STDERR 'Adding config: ' . $key . ' => ' . $val . "\n";
  142. }
  143. } else {
  144. warn('Ignoring config line: ' . $_ . "\n");
  145. }
  146. }
  147. close($fh);
  148. }
  149. # Phantom setup
  150. my $SCRIPT = undef();
  151. # Open the log if enabled
  152. if ($SYSLOG) {
  153. openlog(basename($0), '', LOG_DAEMON);
  154. }
  155. # Read the torrent excludes list
  156. my %EXCLUDES = ();
  157. if ($EXCLUDES_FILE && -r $EXCLUDES_FILE) {
  158. my $fh;
  159. open($fh, '<', $EXCLUDES_FILE)
  160. or die('Unable to open excludes file: ' . $EXCLUDES_FILE . ': ' . $! . "\n");
  161. while (<$fh>) {
  162. # Skip blank lines and comments
  163. if (/^\s*$/ || /^\s*#/) {
  164. next;
  165. }
  166. # Assume everything else is one BT hash per line
  167. if (/^\s*(\w+)\s*$/) {
  168. my $hash = lc($1);
  169. if ($DEBUG > 1) {
  170. print STDERR 'Excluding hash: ' . $hash . "\n";
  171. }
  172. $EXCLUDES{$hash} = 1;
  173. } else {
  174. die('Invalid exclude line: ' . $_ . "\n");
  175. }
  176. }
  177. close($fh);
  178. } elsif ($DEBUG) {
  179. print STDERR 'No excludes file available';
  180. if ($EXCLUDES_FILE) {
  181. print STDERR ': ' . $EXCLUDES_FILE;
  182. }
  183. print STDERR "\n";
  184. }
  185. # Figure out what we're searching for
  186. my $show = '';
  187. my @urls = ();
  188. my $CUSTOM_SEARCH = 0;
  189. my $season = 0;
  190. my @need = ();
  191. my %need = ();
  192. # Clean up the input "directory" path
  193. {
  194. # Allow use of the raw series name
  195. if (!($dir =~ /\//)) {
  196. $show = $dir;
  197. $dir = $TV_DIR . '/' . $dir;
  198. }
  199. # Allow use of relative paths
  200. $dir = File::Spec->rel2abs($dir);
  201. # Sanity check
  202. if (!-d $dir) {
  203. die('Invalid input directory: ' . $dir . "\n");
  204. }
  205. # Isolate the season from the path, if provided
  206. $dir =~ /\/Season\s+(\d+)\/?$/i;
  207. if ($1) {
  208. $season = $1;
  209. $dir = dirname($dir);
  210. }
  211. # If no season is provided find the latest
  212. if (!$season) {
  213. opendir(SERIES, $dir)
  214. or die("Unable to open series directory: ${!}\n");
  215. while (my $file = readdir(SERIES)) {
  216. if ($file =~ /^Season\s+(\d+)$/i) {
  217. if (!$season || $season < $1) {
  218. $season = $1;
  219. }
  220. }
  221. }
  222. closedir(SERIES);
  223. }
  224. # Isolate and clean the series name
  225. if (!$show) {
  226. $show = basename($dir);
  227. }
  228. $show =~ s/[\'\"\.]//g;
  229. if ($DEBUG) {
  230. print STDERR 'Checking directory: ' . $dir . "\n";
  231. }
  232. if ($SYSLOG) {
  233. syslog(LOG_NOTICE, $show);
  234. }
  235. }
  236. # Allow the show name to be overriden
  237. {
  238. my $search_name = $dir . '/search_name';
  239. if (-e $search_name) {
  240. local ($/, *FH);
  241. open(FH, $search_name)
  242. or die('Unable to read search_name for show: ' . $show . ': ' . $! . "\n");
  243. my $text = <FH>;
  244. close(FH);
  245. if ($text =~ /^\s*(\S.*\S)\s*$/) {
  246. $show = $1;
  247. } else {
  248. print STDERR 'Skipping invalid search_name for show: ' . $show . ': ' . $text . "\n";
  249. }
  250. }
  251. if ($DEBUG) {
  252. print STDERR 'Searching with series title: ' . $show . "\n";
  253. }
  254. }
  255. # Allow quality checks to be disabled
  256. if (-e $dir . '/no_quality_checks') {
  257. $NO_QUALITY_CHECKS = 1;
  258. if ($DEBUG) {
  259. print STDERR 'Searching with no quality checks: ' . $show . "\n";
  260. }
  261. }
  262. # Allow use of more number formats
  263. if (-e $dir . '/more_number_formats') {
  264. $MORE_NUMBER_FORMATS = 1;
  265. if ($DEBUG) {
  266. print STDERR 'Searching with more number formats: ' . $show . "\n";
  267. }
  268. }
  269. # Read the search excludes file, if any
  270. my $exclude = '';
  271. my %TITLE_EXCLUDES = ();
  272. if (-e $dir . '/excludes') {
  273. local $/ = undef;
  274. open(EX, $dir . '/excludes')
  275. or die("Unable to open series excludes file: ${!}\n");
  276. my $ex = <EX>;
  277. close(EX);
  278. $ex =~ s/^\s+//;
  279. $ex =~ s/\s+$//;
  280. my @excludes = split(/\s*,\s*/, $ex);
  281. foreach my $ex (@excludes) {
  282. $TITLE_EXCLUDES{$ex} = 1;
  283. if (length($exclude)) {
  284. $exclude .= ' ';
  285. }
  286. $exclude .= '-"' . $ex . '"';
  287. }
  288. $exclude = uri_encode(' ' . $exclude);
  289. }
  290. # Setup our sources
  291. my $SOURCES = initSources();
  292. # Adjust the inter-page delay with respect to the number of unique sources
  293. $DELAY /= scalar(keys(%{$SOURCES})) / 2;
  294. # Handle custom searches
  295. if ((scalar(@urls) < 1) && defined($search) && length($search) > 0) {
  296. # Note the custom search string
  297. $CUSTOM_SEARCH = 1;
  298. if ($DEBUG) {
  299. print STDERR "Custom search\n";
  300. }
  301. # Create the relevent search strings
  302. foreach my $key (keys(%{$SOURCES})) {
  303. my $source = $SOURCES->{$key};
  304. push(@urls, $source->{'search_url'} . $search . $exclude . $source->{'search_suffix'});
  305. }
  306. }
  307. # Handle search-by-date series
  308. if ((scalar(@urls) < 1) && -e $dir . '/search_by_date') {
  309. # Note the search-by-date status
  310. # Set the CUSTOM_SERACH flag to skip season/epsiode matching
  311. $CUSTOM_SEARCH = 1;
  312. # Read the find-by-date string
  313. my $search_by_date = '';
  314. local ($/, *FH);
  315. open(FH, $dir . '/search_by_date')
  316. or die('Unable to read search_by_date for show: ' . $show . ': ' . $! . "\n");
  317. my $text = <FH>;
  318. close(FH);
  319. if ($text =~ /^\s*(\S.*\S)\s*$/) {
  320. $search_by_date = $1;
  321. } else {
  322. die('Skipping invalid search_by_date for show: ' . $show . ': ' . $text . "\n");
  323. }
  324. # Create search strings for each date in the range, unless the related file already exists
  325. my (%years, %months, %days) = ();
  326. for (my $days_back = $MIN_DAYS_BACK ; $days_back <= $MAX_DAYS_BACK ; $days_back++) {
  327. # Calculate the date
  328. my (undef(), undef(), undef(), $day, $month, $year) = localtime(time() - (86400 * $days_back));
  329. # Format as strings
  330. $year = sprintf('%04d', $year + 1900);
  331. $month = sprintf('%02d', $month + 1);
  332. $day = sprintf('%02d', $day);
  333. # Check for an existing file
  334. my $exists = 0;
  335. {
  336. my $season_dir = $dir . '/Season ' . $year;
  337. my $prefix = qr/${year}\-${month}\-${day}\s*\-\s*/;
  338. opendir(SEASON, $season_dir)
  339. or die('Unable to open season directory (' . $season_dir . '): ' . $! . "\n");
  340. while (my $file = readdir(SEASON)) {
  341. if ($file =~ $prefix) {
  342. $exists = 1;
  343. last;
  344. }
  345. }
  346. closedir(SEASON);
  347. }
  348. if ($exists) {
  349. next;
  350. }
  351. # Save all the date string components
  352. $years{$year} = 1;
  353. $months{$month} = 1;
  354. $days{$day} = 1;
  355. # Create the relevent search strings
  356. my $search_str = $search_by_date;
  357. $search_str =~ s/%Y/${year}/g;
  358. $search_str =~ s/%m/${month}/g;
  359. $search_str =~ s/%d/${day}/g;
  360. foreach my $key (keys(%{$SOURCES})) {
  361. my $source = $SOURCES->{$key};
  362. push(@urls, $source->{'search_url'} . $search_str . $exclude . $source->{'search_suffix'});
  363. }
  364. }
  365. # Build a date string matching regex
  366. my $str = '\b(?:' . join('|', keys(%years)) . ')\b';
  367. $str .= '.*';
  368. $str .= '\b(?:' . join('|', keys(%months)) . ')\b';
  369. $str .= '.*';
  370. $str .= '\b(?:' . join('|', keys(%days)) . ')\b';
  371. $CUSTOM_SEARCH = qr/${str}/;
  372. # Debug
  373. if ($DEBUG) {
  374. print STDERR 'Searching with date template: ' . $str . "\n";
  375. }
  376. }
  377. # Handle standard series
  378. if (scalar(@urls) < 1) {
  379. # Validate the season number
  380. if (!defined($season) || $season < 1 || $season > 2000) {
  381. die('Invalid season number: ' . $show . ' => ' . $season . "\n");
  382. }
  383. # Get the last episode number
  384. my $no_next = 0;
  385. my %episodes = ();
  386. my $highest = 0;
  387. opendir(SEASON, $dir . '/Season ' . $season)
  388. or die("Unable to open season directory: ${!}\n");
  389. while (my $file = readdir(SEASON)) {
  390. # Skip support files
  391. if ($file =~ /\.(?:png|xml|jpg|gif|tbn|txt|nfo|torrent)\s*$/i) {
  392. next;
  393. }
  394. # Check for a season_done file
  395. if ($file eq 'season_done') {
  396. $no_next = 1;
  397. next;
  398. }
  399. # Extract the episode number
  400. my ($num) = $file =~ /^\s*\.?(\d+)\s*\-\s*/i;
  401. if (defined($num)) {
  402. $num = int($num);
  403. if ($DEBUG) {
  404. print STDERR 'Found episode number: ' . $num . ' in file: ' . $file . "\n";
  405. }
  406. # Record it
  407. $episodes{$num} = 1;
  408. # Track the highest episode number
  409. if ($num > $highest) {
  410. $highest = $num;
  411. }
  412. }
  413. }
  414. close(SEASON);
  415. # Assume we need the next 2 episodes, unless no_next is set (i.e. season_done)
  416. if (!$no_next) {
  417. for (my $i = 1 ; $i <= $NEXT_EPISODES ; $i++) {
  418. push(@need, $highest + $i);
  419. }
  420. }
  421. # Find any missing episodes
  422. for (my $i = 1 ; $i <= $highest ; $i++) {
  423. if (!$episodes{$i}) {
  424. push(@need, $i);
  425. }
  426. }
  427. if ($DEBUG) {
  428. print STDERR 'Needed episodes: ' . join(', ', @need) . "\n";
  429. }
  430. # Reverse the array for later matching
  431. foreach my $episode (@need) {
  432. $need{$episode} = 1;
  433. }
  434. # Construct a URL-friendly show name
  435. my $safeShow = $show;
  436. $safeShow =~ s/\s+\&\s+/ and /i;
  437. $safeShow =~ s/^\s*The\b//i;
  438. $safeShow =~ s/\s+\-\s+/ /g;
  439. $safeShow =~ s/[\'\:]//g;
  440. $safeShow =~ s/[^\w\"\-]+/ /g;
  441. $safeShow =~ s/^\s+//;
  442. $safeShow =~ s/\s+$//;
  443. $safeShow =~ s/\s\s+/ /g;
  444. $safeShow =~ s/([^A-Za-z0-9])/sprintf("%%%02X", ord($1))/seg;
  445. $safeShow =~ s/\%20/\+/g;
  446. # Calculate possible name variations
  447. my @urlShowVarients = ();
  448. {
  449. # Default name
  450. push(@urlShowVarients, $safeShow);
  451. # Search for both "and" and "&"
  452. if ($safeShow =~ /\+and\+/i) {
  453. my $tmp = $safeShow;
  454. $tmp =~ s/\+and\+/\+%26\+/;
  455. push(@urlShowVarients, $tmp);
  456. }
  457. }
  458. # Construct the URL for each title varient of each needed episode
  459. foreach my $urlShow (@urlShowVarients) {
  460. foreach my $episode (@need) {
  461. my $episode_long = sprintf('%02d', $episode);
  462. my $season_long = sprintf('%02d', $season);
  463. foreach my $source (values(%{$SOURCES})) {
  464. # Allow custom handling
  465. if (defined($source->{'custom_search'}) && ref($source->{'custom_search'}) eq 'CODE') {
  466. $source->{'custom_search'}->(\@urls, $urlShow, $season, $episode);
  467. next;
  468. }
  469. # Use quotes around the show name if the source needs them
  470. my $quote = '%22';
  471. if (!$source->{'quote'}) {
  472. $quote = '';
  473. }
  474. # Calculate the compete search prefix and suffix to simplify later concatenations
  475. my $prefix = $source->{'search_url'} . $quote . $urlShow . $quote;
  476. my $suffix = '';
  477. if ($source->{'search_exclude'}) {
  478. $suffix .= $exclude;
  479. }
  480. if ($source->{'search_suffix'}) {
  481. $suffix .= $source->{'search_suffix'};
  482. }
  483. # SXXEYY
  484. my $url = $prefix . '+s' . $season_long . 'e' . $episode_long . $suffix;
  485. push(@urls, $url);
  486. # Extra searches for shows that have lazy/non-standard number formats
  487. if ($MORE_NUMBER_FORMATS) {
  488. # SXEY
  489. if ($season_long ne $season || $episode_long ne $episode) {
  490. $url = $prefix . '+s' . $season . 'e' . $episode . $suffix;
  491. push(@urls, $url);
  492. }
  493. # SXX EYY
  494. $url = $prefix . '+s' . $season_long . '+e' . $episode_long . $suffix;
  495. push(@urls, $url);
  496. # Season XX Episode YY
  497. $url = $prefix . '+season+' . $season_long . '+episode+' . $episode_long . $suffix;
  498. push(@urls, $url);
  499. # Series X Episode Y
  500. $url = $prefix . '+series+' . $season . '+episode+' . $episode . $suffix;
  501. push(@urls, $url);
  502. # SxEE
  503. $url = $prefix . '+' . $season . 'x' . $episode_long . $suffix;
  504. push(@urls, $url);
  505. # Season X
  506. if ($NO_QUALITY_CHECKS) {
  507. $url = $prefix . '+Season+' . $season . $suffix;
  508. push(@urls, $url);
  509. }
  510. }
  511. }
  512. }
  513. }
  514. }
  515. my @html_content = ();
  516. my @json_content = ();
  517. foreach my $url (@urls) {
  518. # Fetch the page
  519. my $errCount = 0;
  520. my $content = '';
  521. HTTP_ERR_LOOP: {
  522. my $code = 0;
  523. if ($DEBUG) {
  524. print STDERR 'Searching with URL: ' . $url . "\n";
  525. }
  526. # Fetch
  527. ($content, $code) = fetch($url, 'primary.html');
  528. # Check for useful errors
  529. if ($code == 404) {
  530. if ($DEBUG) {
  531. print STDERR "Skipping content from 404 response\n";
  532. }
  533. next;
  534. }
  535. # Check for less useful errors
  536. if ($code != 200) {
  537. if ($code >= 500 && $code <= 599) {
  538. if ($errCount > $ERR_RETRIES) {
  539. print STDERR 'Unable to fetch URL: ' . $url . "\n";
  540. $errCount = 0;
  541. next;
  542. }
  543. if ($DEBUG) {
  544. print STDERR 'Retrying URL (' . $code . '): ' . $url . "\n";
  545. }
  546. $errCount++;
  547. sleep($ERR_DELAY);
  548. redo HTTP_ERR_LOOP;
  549. } else {
  550. print STDERR 'Error fetching URL (' . $code . '): ' . $url . "\n";
  551. }
  552. next;
  553. }
  554. }
  555. # Save the content, discriminating by data type
  556. if ($DEBUG) {
  557. print STDERR 'Fetched ' . length($content) . " bytes\n";
  558. }
  559. if ($content =~ /^\s*{/) {
  560. my $json = eval { decode_json($content); };
  561. if (defined($json) && ref($json)) {
  562. push(@json_content, $json);
  563. } elsif ($DEBUG) {
  564. print STDERR 'JSON parsing failure on: ' . $content . "\n";
  565. }
  566. } else {
  567. push(@html_content, scalar($content));
  568. }
  569. }
  570. # We need someplace to store parsed torrent data from the various sources
  571. my @tors = ();
  572. # Handle HTML content
  573. foreach my $content (@html_content) {
  574. if ($content =~ /\<title\>The Pirate Bay/i) {
  575. # Find TR elements from TPB
  576. my @trs = splitTags($content, 'TR', 'TH', undef());
  577. foreach my $tr (@trs) {
  578. # Find the show title
  579. my ($title) = $tr =~ /title\=\"Details\s+for\s+([^\"]*)\"/i;
  580. if (!defined($title) || length($title) < 1) {
  581. if ($DEBUG) {
  582. print STDERR "Unable to find show title in TR\n\t" . $tr . "\n";
  583. }
  584. next;
  585. }
  586. $title =~ s/^\s+//;
  587. # Extract the season and episode numbers
  588. my ($fileSeason, $episode) = findSE($title);
  589. # Extract the URL
  590. my ($url) = $tr =~ /\<a\s+href\=\"(magnet\:\?[^\"]+)\"/i;
  591. if (!defined($url) || length($url) < 1) {
  592. if ($DEBUG) {
  593. print STDERR "Skipping TR with no magnet URL\n";
  594. }
  595. next;
  596. }
  597. $url = decode_entities($url);
  598. # Count the sum of seeders and leachers
  599. my $seeds = 0;
  600. my $leaches = 0;
  601. if ($tr =~ /\<td(?:\s+[^\>]*)?\>(\d+)\<\/td\>\s*\<td(?:\s+[^\>]*)?\>(\d+)\<\/td\>\s*$/i) {
  602. $seeds = $1;
  603. $leaches = $2;
  604. }
  605. # Extract the size (from separate column or inline)
  606. my $size = 0;
  607. my $unit = 'M';
  608. if ($tr =~ m/Size (\d+(?:\.\d+)?)\&nbsp\;(G|M)iB/) {
  609. $size = $1;
  610. $unit = $2;
  611. } elsif ($tr =~ m/(\d+(?:\.\d+)?)\&nbsp\;(G|M)iB\<\/[tT][dD]\>/) {
  612. $size = $1;
  613. $unit = $2;
  614. }
  615. if ($unit eq 'G') {
  616. $size *= 1024;
  617. }
  618. $size = int($size);
  619. if ($DEBUG) {
  620. print STDERR 'Found file (' . $title . '): ' . $url . "\n";
  621. }
  622. # Save the extracted data
  623. my %tor = (
  624. 'title' => $title,
  625. 'season' => $fileSeason,
  626. 'episode' => $episode,
  627. 'seeds' => $seeds,
  628. 'leaches' => $leaches,
  629. 'size' => $size,
  630. 'url' => $url,
  631. 'source' => 'TPB'
  632. );
  633. push(@tors, \%tor);
  634. }
  635. } elsif ($content =~ /\<title\>[^\<\>]*\bisoHunt\b[^\<\>]*\<\/title\>/i) {
  636. # Find each TR element from ISOHunt
  637. my @trs = splitTags($content, 'TR', 'TH', undef());
  638. foreach my $tr (@trs) {
  639. # Split each TD element from the row
  640. my @tds = split(/\<td(?:\s+[^\>]*)?\>/i, $tr);
  641. if (scalar(@tds) > 10 || scalar(@tds) < 9) {
  642. if ($tds[1] =~ /No results found/) {
  643. next;
  644. }
  645. print STDERR 'Skipping invalid ISO TR: ' . $tr . "\n";
  646. for (my $i = 0 ; $i < scalar(@tds) ; $i++) {
  647. print STDERR 'TD' . $i . ': ' . $tds[$i] . "\n";
  648. }
  649. next;
  650. }
  651. # Skip sponsored results
  652. if ($tds[1] =~ /\bSponsored\b/i) {
  653. if ($DEBUG > 1) {
  654. print STDERR "Skipping sponsored result\n";
  655. }
  656. next;
  657. }
  658. # Find the torrent ID and show title
  659. my ($id, $title) = $tds[2] =~ /\<a(?:\s+[^\>]*)?href=\"\/torrent_details\/(\d+\/[^\"]+)\"\>\<span\>([^\>]+)\<\/span\>/i;
  660. if (!defined($id) || length($id) < 1 || !defined($title) || length($title) < 1) {
  661. if ($DEBUG > 1) {
  662. print STDERR "Unable to find show ID or title in TD:\n\t" . $tds[2] . "\n";
  663. }
  664. next;
  665. }
  666. $title =~ s/^\s+//;
  667. # Extract the season and episode numbers
  668. my ($fileSeason, $episode) = findSE($title);
  669. # Extract the size
  670. my $size = 0;
  671. my $unit = 'M';
  672. if ($tds[6] =~ m/(\d+(?:\.\d+)?) (G|M)B<\/td\>/i) {
  673. $size = $1;
  674. $unit = $2;
  675. }
  676. if ($unit eq 'G') {
  677. $size *= 1024;
  678. }
  679. $size = int($size);
  680. # Count the sum of seeders and leachers
  681. my $seeds = 0;
  682. if ($tds[7] =~ /(\d+)\<\/td\>/i) {
  683. $seeds = $1;
  684. }
  685. my $leaches = 0;
  686. if ($tds[8] =~ /(\d+)\<\/td\>/i) {
  687. $leaches = $1;
  688. }
  689. # Build the detail page URL
  690. my $url = $SOURCES->{'ISO'}->{'protocol'} . '://' . $SOURCES->{'ISO'}->{'host'} . '/torrent_details/' . $id;
  691. if ($DEBUG) {
  692. print STDERR 'Found file (' . $title . '): ' . $url . "\n";
  693. }
  694. # Save the extracted data
  695. my %tor = (
  696. 'title' => $title,
  697. 'season' => $fileSeason,
  698. 'episode' => $episode,
  699. 'seeds' => $seeds,
  700. 'leaches' => $leaches,
  701. 'size' => $size,
  702. 'url' => $url,
  703. 'source' => 'ISO'
  704. );
  705. push(@tors, \%tor);
  706. }
  707. } elsif ($content =~ /Kickass\s*Torrents\<\/title\>/i) {
  708. # Find each TR element from Kickass
  709. my @trs = splitTags($content, 'TR', 'TH', undef());
  710. foreach my $tr (@trs) {
  711. # Split each TD element from the row
  712. my @tds = split(/\<td(?:\s+[^\>]*)?\>/i, $tr);
  713. if (scalar(@tds) != 6) {
  714. print STDERR "Invalid KICK TR:\n";
  715. for (my $i = 0 ; $i < scalar(@tds) ; $i++) {
  716. print STDERR 'TD' . $i . ': ' . $tds[$i] . "\n";
  717. }
  718. next;
  719. }
  720. # Skip empty results
  721. if ( $tds[1] =~ /\<h2\>Nothing found\!\<\/h2\>/i
  722. || $tds[1] =~ /\<p\>\<strong\>Page not found\<\/strong\>\<\/p\>/i)
  723. {
  724. if ($DEBUG) {
  725. print STDERR "Skipping empty TD\n";
  726. }
  727. next;
  728. }
  729. # Find the torrent title
  730. my ($title) = $tds[1] =~ /\<a(?:\s+[^\>]*)?href\=\"[^\"]+\?title\=\[[^\]\"]+\]([^\"]+)\"/i;
  731. if (!defined($title) || length($title) < 1) {
  732. if ($DEBUG) {
  733. print STDERR "Unable to find torrent title in TD:\n\t" . $tds[1] . "\n";
  734. }
  735. next;
  736. }
  737. $title =~ s/^\s+//;
  738. # Find the torrent URL
  739. my ($url) = $tds[1] =~ /\<a(?:\s+[^\>]*)?href=\"(magnet:[^\"]+)\"/i;
  740. if (!defined($url) || length($url) < 1) {
  741. if ($DEBUG) {
  742. print STDERR "Unable to find torrent URL in TD:\n\t" . $tds[1] . "\n";
  743. }
  744. next;
  745. }
  746. $url = decode_entities($url);
  747. # Extract the season and episode numbers
  748. my ($fileSeason, $episode) = findSE($title);
  749. # Extract the size
  750. my $size = 0;
  751. my $unit = 'M';
  752. if ($tds[2] =~ m/(\d+(?:\.\d+)?)\s+\<span\>(G|M)B\<\/span\>\<\/td\>/i) {
  753. $size = $1;
  754. $unit = $2;
  755. }
  756. if ($unit eq 'G') {
  757. $size *= 1024;
  758. }
  759. $size = int($size);
  760. # Count the sum of seeders and leachers
  761. my $seeds = 0;
  762. if ($tds[4] =~ /(\d+)\<\/td\>/i) {
  763. $seeds = $1;
  764. }
  765. my $leaches = 0;
  766. if ($tds[5] =~ /(\d+)\<\/td\>/i) {
  767. $leaches = $1;
  768. }
  769. if ($DEBUG) {
  770. print STDERR 'Found file (' . $title . '): ' . $url . "\n";
  771. }
  772. # Save the extracted data
  773. my %tor = (
  774. 'title' => $title,
  775. 'season' => $fileSeason,
  776. 'episode' => $episode,
  777. 'seeds' => $seeds,
  778. 'leaches' => $leaches,
  779. 'size' => $size,
  780. 'url' => $url,
  781. 'source' => 'KICK'
  782. );
  783. push(@tors, \%tor);
  784. }
  785. } elsif ($content =~ /\<a(?:\s+[^\>]*)?\>Torrentz\<\/a\>/i) {
  786. # We'll need to strip HTML
  787. my $hs = HTML::Strip->new();
  788. # Find each DL element from Torrentz
  789. my @dls = splitTags($content, 'DL', undef(), undef());
  790. foreach my $dl (@dls) {
  791. # Skip ads
  792. if ($dl =~ /\s+rel\=\"nofollow/i) {
  793. next;
  794. }
  795. # Find the torrent title
  796. my ($hash, $title) = $dl =~ /\<a href\=\"\/(\w+)\">(.*)\<\/a\>/i;
  797. if (!defined($hash) || length($hash) < 1 || !defined($title) || length($title) < 1) {
  798. if ($DEBUG) {
  799. print STDERR "Unable to find torrent title in DL:\n\t" . $dl . "\n";
  800. }
  801. next;
  802. }
  803. $hash = lc($hash);
  804. $title = $hs->parse($title);
  805. $hs->eof;
  806. $title =~ s/^\s+//;
  807. # Extract the season and episode numbers
  808. my ($fileSeason, $episode) = findSE($title);
  809. # Extract the size
  810. my $size = 0;
  811. if ($dl =~ /\<span class\=\"s\">(\d+(?:\.\d+)?) (G|M)B\<\/span\>/i) {
  812. $size = $1;
  813. if ($2 eq 'G') {
  814. $size *= 1024;
  815. }
  816. $size = int($size);
  817. }
  818. # Count the sum of seeders and leachers
  819. my $seeds = 0;
  820. if ($dl =~ /<span class\=\"u\">(\d+)\<\/span\>/i) {
  821. $seeds = $1;
  822. }
  823. my $leaches = 0;
  824. if ($dl =~ /<span class\=\"d\">(\d+)\<\/span\>/i) {
  825. $leaches = $1;
  826. }
  827. # Construct a magnet URL from the hash
  828. # Assume the seconary lookup will append a list of trackers
  829. my $url = 'magnet:?xt=urn:btih:' . $hash . '&dn=' . uri_encode($title, { 'encode_reserved' => 1 });
  830. if ($DEBUG) {
  831. print STDERR 'Found file (' . $title . '): ' . $url . "\n";
  832. }
  833. # Save the extracted data
  834. my %tor = (
  835. 'title' => $title,
  836. 'season' => $fileSeason,
  837. 'episode' => $episode,
  838. 'seeds' => $seeds,
  839. 'leaches' => $leaches,
  840. 'size' => $size,
  841. 'url' => $url,
  842. 'source' => 'Z'
  843. );
  844. push(@tors, \%tor);
  845. }
  846. } elsif ($content =~ /ExtraTorrent\.cc The World\'s Largest BitTorrent System\<\/title\>/i) {
  847. # Find TR elements from ET
  848. my @trs = splitTags($content, 'TR', undef(), 'magnet\:');
  849. foreach my $tr (@trs) {
  850. # Find the show title
  851. my ($title) = $tr =~ /title\=\"Download\s+([^\"]*)\s+torrent\"/i;
  852. if (!defined($title) || length($title) < 1) {
  853. if ($DEBUG) {
  854. print STDERR "Unable to find show title in TR\n\t" . $tr . "\n";
  855. }
  856. next;
  857. }
  858. $title =~ s/^\s+//;
  859. # Find the magnet URL
  860. my ($url) = $tr =~ /\<a\s+href=\"(magnet\:[^\"]+)\"/i;
  861. if (!defined($url) || length($url) < 1) {
  862. if ($DEBUG) {
  863. print STDERR "Skipping TR with no magnet URL\n";
  864. }
  865. next;
  866. }
  867. $url = decode_entities($url);
  868. # Extract the season and episode numbers
  869. my ($fileSeason, $episode) = findSE($title);
  870. # Count the sum of seeders and leachers
  871. my $seeds = 0;
  872. my $leaches = 0;
  873. if ($tr =~ /\<td\s+class=\"sy\">(\d+)\<\/td\>/) {
  874. $seeds = $1;
  875. }
  876. if ($tr =~ /\<td\s+class=\"ly\">(\d+)\<\/td\>/) {
  877. $leaches = $1;
  878. }
  879. # Extract the size (from separate column or inline)
  880. my $size = 0;
  881. my $unit = 'M';
  882. if ($tr =~ /\<td\>(\d+(?:\.\d+)?)\&nbsp\;(G|M)B\<\/td\>/) {
  883. $size = $1;
  884. $unit = $2;
  885. }
  886. if ($unit eq 'G') {
  887. $size *= 1024;
  888. }
  889. $size = int($size);
  890. if ($DEBUG) {
  891. print STDERR 'Found file (' . $title . '): ' . $url . "\n";
  892. }
  893. # Save the extracted data
  894. my %tor = (
  895. 'title' => $title,
  896. 'season' => $fileSeason,
  897. 'episode' => $episode,
  898. 'seeds' => $seeds,
  899. 'leaches' => $leaches,
  900. 'size' => $size,
  901. 'url' => $url,
  902. 'source' => 'ET'
  903. );
  904. push(@tors, \%tor);
  905. }
  906. } elsif ($content =~ /\<title\>Sorry/) {
  907. if ($DEBUG) {
  908. print STDERR "ISO offline\n";
  909. }
  910. } elsif ($content =~ /Database\s+maintenance/i) {
  911. if ($DEBUG) {
  912. print STDERR "TPB offline\n";
  913. }
  914. } elsif ($content =~ /^\s*$/) {
  915. if ($DEBUG) {
  916. print STDERR "Source returned no content\n";
  917. }
  918. } else {
  919. print STDERR "Unknown HTML content:\n" . $content . "\n\n";
  920. }
  921. }
  922. # Handle JSON content
  923. foreach my $content (@json_content) {
  924. if (exists($content->{'title'}) && $content->{'title'} =~ /NZBCat/i) {
  925. my $list = $content->{'item'};
  926. if (!$list || ref($list) ne 'ARRAY') {
  927. warn("Invalid NCAT list\n");
  928. next;
  929. }
  930. foreach my $item (@{$list}) {
  931. # Ensure the record is sensible
  932. my $id = undef();
  933. if ( ref($item) eq 'HASH'
  934. && exists($item->{'guid'})
  935. && ref($item->{'guid'}) eq 'HASH'
  936. && exists($item->{'guid'}->{'text'}))
  937. {
  938. $id = $item->{'guid'}->{'text'};
  939. }
  940. if (!$id) {
  941. warn("Unable to parse NCAT list item\n");
  942. next;
  943. }
  944. # NZB URL
  945. my $url = undef();
  946. if (exists($item->{'link'}) && $item->{'link'}) {
  947. $url = $item->{'link'};
  948. $url =~ s/^(https?)\:/${PROTOCOL}\:/;
  949. }
  950. # Title
  951. my $title = undef();
  952. if (exists($item->{'title'}) && $item->{'title'}) {
  953. $title = $item->{'title'};
  954. } elsif (exists($item->{'description'}) && $item->{'description'}) {
  955. $title = $item->{'description'};
  956. }
  957. if ($title) {
  958. $url .= '#' . $title;
  959. }
  960. # Extract the season and episode numbers
  961. my ($season, $episode) = findSE($title);
  962. # Size
  963. my $size = 0;
  964. if (exists($item->{'newznab:attr'}) && $item->{'newznab:attr'} && ref($item->{'newznab:attr'}) eq 'ARRAY') {
  965. foreach my $hash (@{ $item->{'newznab:attr'} }) {
  966. if (ref($hash) eq 'HASH' && exists($hash->{'_name'}) && exists($hash->{'_value'}) && $hash->{'_name'} eq 'size')
  967. {
  968. $size = int($hash->{'_value'}) / 1024;
  969. }
  970. }
  971. }
  972. # Date
  973. my $date = undef();
  974. if (exists($item->{'pubDate'}) && $item->{'pubDate'}) {
  975. $date = str2time($item->{'pubDate'});
  976. }
  977. # Sanity checks
  978. if (!$url) {
  979. warn('No URL in NCAT item: ' . $id . "\n");
  980. next;
  981. }
  982. if (!$title) {
  983. warn('No title in NCAT item: ' . $id . "\n");
  984. next;
  985. }
  986. if (!$size) {
  987. warn('No size in NCAT item: ' . $id . "\n");
  988. next;
  989. }
  990. if ($DEBUG) {
  991. print STDERR 'Found file (' . $title . '): ' . $url . "\n";
  992. }
  993. # Save the extracted data
  994. my %tor = (
  995. 'title' => $title,
  996. 'season' => $season,
  997. 'episode' => $episode,
  998. 'date' => $date,
  999. 'size' => $size,
  1000. 'url' => $url,
  1001. 'source' => 'NCAT'
  1002. );
  1003. push(@tors, \%tor);
  1004. }
  1005. } else {
  1006. print STDERR "Unknown JSON content:\n" . $content . "\n\n";
  1007. }
  1008. }
  1009. # Filter for size/count/etc.
  1010. my %tors = ();
  1011. my $showRegex = undef();
  1012. {
  1013. my $showClean = $show;
  1014. $showClean =~ s/[\"\']//g;
  1015. $showClean =~ s/[\W_]+/\[\\W_\].*/g;
  1016. $showRegex = qr/^${showClean}[\W_]/i;
  1017. }
  1018. foreach my $tor (@tors) {
  1019. # Extract the BTIH, if available
  1020. getHash($tor);
  1021. # Skip files that are in the torrent excludes list
  1022. if ($tor->{'hash'} && $EXCLUDES{ $tor->{'hash'} }) {
  1023. if ($DEBUG) {
  1024. print STDERR 'Skipping file: Excluded hash (' . $tor->{'hash'} . '): ' . $tor->{'title'} . "\n";
  1025. }
  1026. next;
  1027. }
  1028. # Skip files that don't start with our show title
  1029. if (!($tor->{'title'} =~ $showRegex)) {
  1030. if ($DEBUG) {
  1031. print STDERR 'Skipping file: Title does not match (' . $showRegex . '): ' . $tor->{'title'} . "\n";
  1032. }
  1033. next;
  1034. # Skip pre-air files
  1035. } elsif ($tor->{'title'} =~ /preair/i) {
  1036. if ($DEBUG) {
  1037. print STDERR 'Skipping file: Title contains "preair": ' . $tor->{'title'} . "\n";
  1038. }
  1039. next;
  1040. # Skip SWESUB files
  1041. } elsif ($tor->{'title'} =~ /swesub/i) {
  1042. if ($DEBUG) {
  1043. print STDERR 'Skipping file: Title contains "SWESUB": ' . $tor->{'title'} . "\n";
  1044. }
  1045. next;
  1046. # Skip .rus. files
  1047. } elsif ($tor->{'title'} =~ /\.rus\./i) {
  1048. if ($DEBUG) {
  1049. print STDERR 'Skipping file: Title contains ".rus.": ' . $tor->{'title'} . "\n";
  1050. }
  1051. next;
  1052. # Skip German files
  1053. } elsif ($tor->{'title'} =~ /german/i) {
  1054. if ($DEBUG) {
  1055. print STDERR 'Skipping file: Title contains "german": ' . $tor->{'title'} . "\n";
  1056. }
  1057. next;
  1058. # Skip French files
  1059. } elsif ($tor->{'title'} =~ /french/i) {
  1060. if ($DEBUG) {
  1061. print STDERR 'Skipping file: Title contains "french": ' . $tor->{'title'} . "\n";
  1062. }
  1063. next;
  1064. # Skip Spanish files
  1065. } elsif ($tor->{'title'} =~ /Español/i) {
  1066. if ($DEBUG) {
  1067. print STDERR 'Skipping file: Title contains "Español": ' . $tor->{'title'} . "\n";
  1068. }
  1069. next;
  1070. # Skip Castellano files
  1071. } elsif ($tor->{'title'} =~ /Castellano/i) {
  1072. if ($DEBUG) {
  1073. print STDERR 'Skipping file: Title contains "Castellano": ' . $tor->{'title'} . "\n";
  1074. }
  1075. next;
  1076. # Skip unedited files
  1077. } elsif ($tor->{'title'} =~ /unedited/i) {
  1078. if ($DEBUG) {
  1079. print STDERR 'Skipping file: Title contains "unedited": ' . $tor->{'title'} . "\n";
  1080. }
  1081. next;
  1082. }
  1083. # Skip files that contain a word from the series exclude list
  1084. my $exclude = undef();
  1085. foreach my $ex (keys(%TITLE_EXCLUDES)) {
  1086. $ex = quotemeta($ex);
  1087. if ($tor->{'title'} =~ m/${ex}/i) {
  1088. $exclude = $ex;
  1089. last;
  1090. }
  1091. }
  1092. if (defined($exclude)) {
  1093. if ($DEBUG) {
  1094. print STDERR 'Skipping file: Title contains "' . $exclude . '": ' . $tor->{'title'} . "\n";
  1095. }
  1096. next;
  1097. }
  1098. # Enforce season and episode number matches for standard searches, or CUSTOM_SEARCH matching (if it's a regex)
  1099. if (!$CUSTOM_SEARCH) {
  1100. # Skip files that don't contain the right season number
  1101. if (!defined($tor->{'season'}) || $tor->{'season'} != $season) {
  1102. if ($DEBUG) {
  1103. print STDERR 'Skipping file: No match for season number (' . $season . '): ' . $tor->{'title'} . "\n";
  1104. }
  1105. next;
  1106. # Skip files that don't contain a needed episode number, unless there is no episode number and NO_QUALITY_CHECKS is set
  1107. } elsif ((defined($tor->{'episode'}) && !$need{ $tor->{'episode'} })
  1108. || (!defined($tor->{'episode'}) && !$NO_QUALITY_CHECKS))
  1109. {
  1110. if ($DEBUG) {
  1111. print STDERR 'Skipping file: No match for episode number (' . $tor->{'episode'} . '): ' . $tor->{'title'} . "\n";
  1112. }
  1113. next;
  1114. }
  1115. } elsif (ref($CUSTOM_SEARCH) eq 'Regexp') {
  1116. # Skip files that don't match the regex
  1117. if (!($tor->{'title'} =~ $CUSTOM_SEARCH)) {
  1118. if ($DEBUG) {
  1119. print STDERR 'Skipping file: No match for CUSTOM_SEARCH regex (' . $CUSTOM_SEARCH . '): ' . $tor->{'title'} . "\n";
  1120. }
  1121. next;
  1122. }
  1123. }
  1124. # Only apply the quality rules if NO_QUALITY_CHECKS is not in effect
  1125. if (!$NO_QUALITY_CHECKS) {
  1126. # Proxy publication date to seeder/leacher quality
  1127. if ($tor->{'date'} && !$tor->{'seeds'}) {
  1128. if ($tor->{'date'} > time() - $QUALITY_AGE) {
  1129. $tor->{'seeds'} = $SEED_RATIO_COUNT + 1;
  1130. } else {
  1131. $tor->{'seeds'} = ($MIN_COUNT / 2) - 1;
  1132. }
  1133. $tor->{'leaches'} = $tor->{'seeds'};
  1134. }
  1135. # Skip torrents with too few seeders/leachers
  1136. if (($tor->{'seeds'} + $tor->{'leaches'}) * $SOURCES->{ $tor->{'source'} }->{'weight'} < $MIN_COUNT) {
  1137. if ($DEBUG) {
  1138. print STDERR 'Skipping file: Insufficient seeder/leacher count ('
  1139. . $tor->{'seeds'} . '/'
  1140. . $tor->{'leaches'} . '): '
  1141. . $tor->{'title'} . "\n";
  1142. }
  1143. next;
  1144. # Skip torrents with unusual seed/leach ratios
  1145. } elsif ($tor->{'seeds'} > 1
  1146. && $tor->{'seeds'} < $SEED_RATIO_COUNT
  1147. && $tor->{'seeds'} > $tor->{'leaches'} * $MAX_SEED_RATIO)
  1148. {
  1149. if ($DEBUG) {
  1150. print STDERR 'Skipping file: Unusual seeder/leacher ratio ('
  1151. . $tor->{'seeds'} . '/'
  1152. . $tor->{'leaches'} . '): '
  1153. . $tor->{'title'} . "\n";
  1154. }
  1155. next;
  1156. # Skip torrents that are too small
  1157. } elsif ($tor->{'size'} < $MIN_SIZE) {
  1158. if ($DEBUG) {
  1159. print STDERR 'Skipping file: Insufficient size (' . $tor->{'size'} . ' MiB): ' . $tor->{'title'} . "\n";
  1160. }
  1161. next;
  1162. }
  1163. }
  1164. # Save good torrents
  1165. push(@{ $tors{ $tor->{'episode'} } }, $tor);
  1166. if ($DEBUG) {
  1167. print STDERR 'Possible URL ('
  1168. . $tor->{'seeds'} . '/'
  1169. . $tor->{'leaches'}
  1170. . ' seeds/leaches, '
  1171. . $tor->{'size'}
  1172. . ' MiB): '
  1173. . $tor->{'title'} . "\n";
  1174. }
  1175. }
  1176. # Find the average torrent size for each episode
  1177. my %size = ();
  1178. {
  1179. my %max = ();
  1180. my %avg = ();
  1181. foreach my $episode (keys(%tors)) {
  1182. my $count = 0;
  1183. $avg{$episode} = 0.001;
  1184. $max{$episode} = 0.001;
  1185. foreach my $tor (@{ $tors{$episode} }) {
  1186. $count++;
  1187. $avg{$episode} += $tor->{'size'};
  1188. if ($tor->{'size'} > $max{$episode}) {
  1189. $max{$episode} = $tor->{'size'};
  1190. }
  1191. }
  1192. if ($count > 0) {
  1193. $avg{$episode} /= $count;
  1194. }
  1195. $size{$episode} = ($max{$episode} + $avg{$episode}) / 2;
  1196. if ($DEBUG) {
  1197. print STDERR 'Episode '
  1198. . $episode
  1199. . ' max/avg/cmp size: '
  1200. . int($max{$episode}) . '/'
  1201. . int($avg{$episode}) . '/'
  1202. . int($size{$episode})
  1203. . " MiB\n";
  1204. }
  1205. }
  1206. }
  1207. # Calculate an adjusted count based on the peer count, relative file size, and title contents
  1208. foreach my $episode (keys(%tors)) {
  1209. foreach my $tor (@{ $tors{$episode} }) {
  1210. my $count = $tor->{'seeds'} + $tor->{'leaches'};
  1211. # Start with the peer count
  1212. $tor->{'adj_count'} = $count;
  1213. # Adjust based on file size
  1214. {
  1215. my $size_ratio = $tor->{'size'} / $size{$episode};
  1216. if ($tor->{'size'} >= $size{$episode}) {
  1217. $tor->{'adj_count'} *= $SIZE_BONUS * $size_ratio;
  1218. } else {
  1219. $tor->{'adj_count'} *= (1 / $SIZE_PENALTY) * $size_ratio;
  1220. }
  1221. }
  1222. # Adjust based on title contents
  1223. if ($tor->{'title'} =~ /Subtitulado/i) {
  1224. $tor->{'adj_count'} *= 1 / $TITLE_PENALTY;
  1225. }
  1226. # Truncate to an integer
  1227. $tor->{'adj_count'} = int($tor->{'adj_count'});
  1228. if ($DEBUG) {
  1229. print STDERR 'Possible URL (' . $tor->{'adj_count'} . ' size-adjusted sources): ' . $tor->{'url'} . "\n";
  1230. }
  1231. }
  1232. }
  1233. # Pick the best-adjusted-count torrent for each episode
  1234. my %urls = ();
  1235. foreach my $episode (keys(%tors)) {
  1236. my @sorted = sort { $b->{'adj_count'} <=> $a->{'adj_count'} } @{ $tors{$episode} };
  1237. my $max = undef();
  1238. foreach my $tor (@sorted) {
  1239. if (!defined($max) || $tor->{'adj_count'} > $max) {
  1240. if ($urls{$episode} = resolveSecondary($tor)) {
  1241. if (!$tor->{'hash'} || $EXCLUDES{ $tor->{'hash'} }) {
  1242. print STDERR 'Double-skipping file: Excluded hash (' . $tor->{'hash'} . '): ' . $tor->{'title'} . "\n";
  1243. next;
  1244. }
  1245. $max = $tor->{'adj_count'};
  1246. }
  1247. if ($DEBUG) {
  1248. print STDERR 'Semi-final URL (adjusted count: ' . $tor->{'adj_count'} . '): ' . $tor->{'url'} . "\n";
  1249. }
  1250. } elsif ($DEBUG) {
  1251. print STDERR 'Skipping for lesser adjusted count (' . $tor->{'adj_count'} . '): ' . $tor->{'url'} . "\n";
  1252. }
  1253. }
  1254. }
  1255. # Output
  1256. foreach my $episode (keys(%tors)) {
  1257. if (defined($urls{$episode})) {
  1258. $urls{$episode} = resolveTrackers($urls{$episode});
  1259. print $urls{$episode} . "\n";
  1260. if ($SYSLOG) {
  1261. syslog(LOG_NOTICE, $show . ': ' . getHash($urls{$episode}));
  1262. }
  1263. if ($DEBUG) {
  1264. print STDERR 'Final URL: ' . $urls{$episode} . "\n";
  1265. }
  1266. } elsif ($DEBUG) {
  1267. print STDERR 'No URL for: ' . $episode . "\n";
  1268. }
  1269. }
  1270. # Cleanup
  1271. if ($COOKIES) {
  1272. unlink($COOKIES);
  1273. }
  1274. if ($SCRIPT) {
  1275. unlink($SCRIPT);
  1276. }
  1277. if ($SYSLOG) {
  1278. closelog();
  1279. }
  1280. exit(0);
  1281. sub getHash($) {
  1282. my ($tor) = @_;
  1283. my $url;
  1284. # Allow either a URL or a tor hashref
  1285. if (ref($tor) eq 'HASH') {
  1286. $url = $tor->{'url'};
  1287. } else {
  1288. $url = $tor;
  1289. undef($tor);
  1290. }
  1291. # Extract the BTIH hash or NZB ID, if available
  1292. my $hash = undef();
  1293. if ($url =~ /\bxt\=urn\:btih\:(\w+)/i) {
  1294. $hash = lc($1);
  1295. } elsif ($url =~ /\/getnzb\/(\w+)\.nzb\&/i) {
  1296. $hash = lc($1);
  1297. }
  1298. # Save back to the tor, if provided
  1299. if (defined($tor) && defined($hash)) {
  1300. $tor->{'hash'} = lc($hash);
  1301. }
  1302. # Always return the hash, or undef() if none can be found
  1303. return $hash;
  1304. }
  1305. # Resolve secondary URLs
  1306. sub resolveSecondary($) {
  1307. my ($tor) = @_;
  1308. # Fetch the torrent-specific page and extract the magent link
  1309. if ($tor->{'source'} eq 'ISO') {
  1310. my ($content, $code) = fetch($tor->{'url'}, 'secondary.html');
  1311. if ($code != 200) {
  1312. print STDERR 'Error fetching secondary URL: ' . $tor->{'url'} . "\n";
  1313. goto OUT;
  1314. }
  1315. my ($magnet) = $content =~ /\<a\s+href\=\"(magnet\:\?[^\"]+)\"/i;
  1316. if (!defined($magnet) || length($magnet) < 1) {
  1317. if ($DEBUG) {
  1318. print STDERR 'No secondary URL available from: ' . $tor->{'url'} . "\n";
  1319. }
  1320. goto OUT;
  1321. }
  1322. $tor->{'url'} = decode_entities($magnet);
  1323. }
  1324. # Extract the hash
  1325. getHash($tor);
  1326. if (!$tor->{'hash'} || $EXCLUDES{ $tor->{'hash'} }) {
  1327. if ($DEBUG) {
  1328. print STDERR 'Skipping file: Excluded hash (' . $tor->{'hash'} . '): ' . $tor->{'title'} . "\n";
  1329. }
  1330. goto OUT;
  1331. }
  1332. OUT:
  1333. return $tor->{'url'};
  1334. }
  1335. sub resolveTrackers($) {
  1336. my ($url) = @_;
  1337. # Always append a few static trackers to the URI
  1338. if ($url =~ /^magnet\:/i) {
  1339. foreach my $tracker (@TRACKERS) {
  1340. my $arg = '&tr=' . uri_encode($tracker, { 'encode_reserved' => 1 });
  1341. my $match = quotemeta($arg);
  1342. if (!($url =~ /${match}/)) {
  1343. $url .= $arg;
  1344. }
  1345. }
  1346. }
  1347. # Fetch a dynamic tracker list for any hashinfo from TorrentZ
  1348. if ($TRACKER_LOOKUP && defined($SOURCES->{'Z'}) && $url =~ /^magnet\:/ && $url =~ /\bxt\=urn\:btih\:(\w+)/i) {
  1349. my $hash = lc($1);
  1350. my $baseURL = $SOURCES->{'Z'}->{'protocol'} . '://' . $SOURCES->{'Z'}->{'host'};
  1351. my $hashURL = $baseURL . '/' . $hash;
  1352. my ($content, $code) = fetch($hashURL, 'hashinfo.html');
  1353. if ($code != 200) {
  1354. print STDERR 'Error fetching hashinfo URL: ' . $hashURL . "\n";
  1355. return $url;
  1356. }
  1357. # Parse out and fetch the tracker list URL
  1358. my ($list) = $content =~ /\<a rel\=\"nofollow\" href\=\"(\/announcelist_\d+)\"\>/;
  1359. if (!defined($list)) {
  1360. if ($DEBUG) {
  1361. print STDERR 'Unable to find tracker list link at: ' . $hashURL . "\n";
  1362. }
  1363. return $url;
  1364. }
  1365. my $listURL = $baseURL . $list;
  1366. ($content, $code) = fetch($listURL, 'tracker.html');
  1367. if ($code != 200) {
  1368. print STDERR 'Error fetching tracker list URL: ' . $listURL . "\n";
  1369. return $url;
  1370. }
  1371. # Parse the tracker list and append our URL
  1372. foreach my $line ($content) {
  1373. if ($line =~ /^\s*$/) {
  1374. next;
  1375. }
  1376. $line =~ s/^\s+//;
  1377. $line =~ s/\s+$//;
  1378. if ($line =~ /^(?:http|udp)\:/i) {
  1379. my $arg = '&tr=' . uri_encode($line, { 'encode_reserved' => 1 });
  1380. my $match = quotemeta($arg);
  1381. if (!($url =~ /${match}/)) {
  1382. if ($DEBUG) {
  1383. print STDERR 'Adding tracker: ' . $line . "\n";
  1384. }
  1385. $url .= $arg;
  1386. }
  1387. } else {
  1388. print STDERR 'Unknown tracker type: ' . $line . "\n";
  1389. }
  1390. }
  1391. }
  1392. return $url;
  1393. }
  1394. sub splitTags($$$$) {
  1395. my ($content, $tag, $header, $match) = @_;
  1396. # Find each tag element
  1397. my @out = ();
  1398. my @parts = split(/\<${tag}(?:\s+[^\>]*)?\>/i, $content);
  1399. foreach my $part (@parts) {
  1400. # Trim trailing tags and skip things that aren't complete tags
  1401. if (!($part =~ s/\<\/${tag}\>.*$//is)) {
  1402. if ($DEBUG > 1) {
  1403. print STDERR 'Skipping non-tag line: ' . $part . "\n\n";
  1404. }
  1405. next;
  1406. }
  1407. # Skip headers
  1408. if (defined($header) && $part =~ /<${header}(?:\s+[^\>]*)?\>/i) {
  1409. if ($DEBUG > 2) {
  1410. print STDERR 'Skipping header line: ' . $part . "\n\n";
  1411. }
  1412. next;
  1413. }
  1414. # Skip non-matches
  1415. if (defined($match) && !($part =~ m/${match}/i)) {
  1416. if ($DEBUG > 2) {
  1417. print STDERR 'Skipping non-match line: ' . $part . "\n\n";
  1418. }
  1419. next;
  1420. }
  1421. # Save good tags
  1422. push(@out, $part);
  1423. }
  1424. return @out;
  1425. }
  1426. # Extract season
  1427. sub findSE($) {
  1428. my ($name) = @_;
  1429. my $season = 0;
  1430. my $episode = 0;
  1431. my $seasonBlock = '';
  1432. if ($name =~ /(?:\b|_)(20\d\d(?:\.|\-)[01]?\d(?:\.|\-)[0-3]?\d)(?:\b|_)/) {
  1433. $seasonBlock = $1;
  1434. my ($month, $day);
  1435. ($season, $month, $day) = $seasonBlock =~ /(20\d\d)(?:\.|\-)([01]?\d)(?:\.|\-)([0-3]?\d)/;
  1436. $episode = sprintf('%04d-%02d-%02d', $season, $month, $day);
  1437. } elsif ($name =~ /(?:\b|_)(S(?:eason)?[_\s\.\-]*\d{1,2}[_\s\.\-]*E(?:pisode)?[_\s\.]*\d{1,3}(?:E\d{1,3})?)(?:\b|_)/i) {
  1438. $seasonBlock = $1;
  1439. ($season, $episode) = $seasonBlock =~ /S(?:eason)?[_\s\.\-]*(\d{1,2})[_\s\.\-]*E(?:pisode)?[_\s\.\-]*(\d{1,3})/i;
  1440. } elsif ($name =~ /[\[\_\.](\d{1,2}x\d{2,3})[\]\_\.]/i) {
  1441. $seasonBlock = $1;
  1442. ($season, $episode) = $seasonBlock =~ /(\d+)x(\d+)/i;
  1443. } elsif ($name =~ /(?:\b|_)([01]?\d[_\s\.]?[0-3]\d)(?:\b|_)/i) {
  1444. $seasonBlock = $1;
  1445. ($season, $episode) = $seasonBlock =~ /(?:\b|_)([01]?\d)[_\s\.]?([0-3]\d)/i;
  1446. }
  1447. # Return something valid and INTy or UNDEF
  1448. if (!defined($seasonBlock) || $season < 1 || length($episode) < 1 || $episode eq '0') {
  1449. if ($DEBUG) {
  1450. print STDERR 'Could not find seasonBlock in: ' . $name . "\n";
  1451. }
  1452. $season = undef();
  1453. $episode = undef();
  1454. } elsif ($episode =~ /^20\d{2}\-\d{2}\-\d{2}$/) {
  1455. # Episode-by-date
  1456. $season = int($season);
  1457. } else {
  1458. # Catchall
  1459. # This will warn and return undef() (which is valid) on error
  1460. $season = int($season);
  1461. $episode = int($episode);
  1462. }
  1463. return ($season, $episode);
  1464. }
  1465. sub initSources() {
  1466. my %sources = ();
  1467. # NZB Cat
  1468. if ($ENABLE_SOURCE{'NCAT'}) {
  1469. my @proxies = ('nzb.cat/api');
  1470. my $source = findProxy(\@proxies, '\bforgottenpassword\b');
  1471. if ($source && exists($CONFIG{'NCAT_APIKEY'}) && $CONFIG{'NCAT_APIKEY'}) {
  1472. $source->{'weight'} = 1.00;
  1473. $source->{'quote'} = 0;
  1474. $source->{'search_exclude'} = 0;
  1475. $source->{'search_suffix'} = '';
  1476. $source->{'custom_search'} = sub ($$$$) {
  1477. my ($urls, $series, $season, $episode) = @_;
  1478. my $url =
  1479. $source->{'search_url'}
  1480. . '?o=json&t=tvsearch&q='
  1481. . $series
  1482. . '&season='
  1483. . $season . '&ep='
  1484. . $episode
  1485. . '&apikey='
  1486. . $CONFIG{'NCAT_APIKEY'};
  1487. push(@{$urls}, $url);
  1488. };
  1489. $sources{'NCAT'} = $source;
  1490. }
  1491. }
  1492. # The Pirate Bay
  1493. if ($ENABLE_SOURCE{'TPB'}) {
  1494. my @proxies = ('thepiratebay.org/search/', 'tpb.unblocked.co/search/');
  1495. my $source = findProxy(\@proxies, '\bPirate Search\b');
  1496. if ($source) {
  1497. $source->{'weight'} = 1.00;
  1498. $source->{'quote'} = 0;
  1499. $source->{'search_exclude'} = 1;
  1500. $source->{'search_suffix'} = '/0/7/0';
  1501. $sources{'TPB'} = $source;
  1502. }
  1503. }
  1504. # ISOhunt
  1505. if ($ENABLE_SOURCE{'ISO'}) {
  1506. my @proxies = ('isohunt.to/torrents/?ihq=', 'isohunters.net/torrents/?ihq=');
  1507. my $source = findProxy(\@proxies, 'Last\s+\d+\s+files\s+indexed');
  1508. if ($source) {
  1509. $source->{'weight'} = 0.75;
  1510. $source->{'quote'} = 1;
  1511. $source->{'search_exclude'} = 1;
  1512. $source->{'search_suffix'} = '';
  1513. $sources{'ISO'} = $source;
  1514. }
  1515. }
  1516. # Kickass
  1517. if ($ENABLE_SOURCE{'KICK'}) {
  1518. my @proxies = ('kickass.cd/search.php?q=', 'kickass.mx/search.php?q=');
  1519. my $source = findProxy(\@proxies, '/search.php');
  1520. if ($source) {
  1521. $source->{'weight'} = 1.00;
  1522. $source->{'quote'} = 1;
  1523. $source->{'search_exclude'} = 1;
  1524. $source->{'search_suffix'} = '/';
  1525. $sources{'KICK'} = $source;
  1526. }
  1527. }
  1528. # Torrentz
  1529. if ($ENABLE_SOURCE{'Z'}) {
  1530. my @proxies = ('torrentz.eu/search?q=', 'torrentz.me/search?q=', 'torrentz.ch/search?q=', 'torrentz.in/search?q=');
  1531. my $source = findProxy(\@proxies, 'Indexing [\d\,]+ active torrents');
  1532. if ($source) {
  1533. $source->{'weight'} = 1.00;
  1534. $source->{'quote'} = 1;
  1535. $source->{'search_exclude'} = 1;
  1536. $source->{'search_suffix'} = '';
  1537. if (!$NO_QUALITY_CHECKS) {
  1538. $source->{'search_suffix'} = '+peer+%3E+' . $MIN_COUNT,;
  1539. }
  1540. $sources{'Z'} = $source;
  1541. }
  1542. }
  1543. # ExtraTorrent
  1544. if ($ENABLE_SOURCE{'ET'}) {
  1545. my @proxies = (
  1546. 'extra.to/search/?search=', 'etmirror.com/search/?search=',
  1547. 'etproxy.com/search/?search=', 'extratorrentlive.com/search/?search='
  1548. );
  1549. my $source = findProxy(\@proxies, '/search/');
  1550. if ($source) {
  1551. $source->{'weight'} = 1.00;
  1552. $source->{'quote'} = 1;
  1553. $source->{'search_exclude'} = 0;
  1554. $source->{'search_suffix'} = '&new=1&x=0&y=0&srt=seeds&order=desc';
  1555. $source->{'search_url'} = 'phantomjs:' . $source->{'search_url'};
  1556. $sources{'ET'} = $source;
  1557. }
  1558. }
  1559. # Sanity check
  1560. if (scalar(keys(%sources)) < 1) {
  1561. die("No sources available\n");
  1562. }
  1563. return \%sources;
  1564. }
  1565. sub findProxy($$) {
  1566. my ($proxies, $match) = @_;
  1567. # Automatically select a proxy
  1568. my $host = '';
  1569. my $protocol = '';
  1570. my $search_url = '';
  1571. foreach my $url (@{$proxies}) {
  1572. my $path;
  1573. ($protocol, $host, $path) = $url =~ /^(?:(https?)\:\/\/)?([^\/]+)(\/.*)?$/i;
  1574. if (!$protocol) {
  1575. $protocol = $PROTOCOL;
  1576. }
  1577. if (!$host) {
  1578. print STDERR 'Could not parse proxy URL: ' . $url . "\n";
  1579. next;
  1580. }
  1581. my ($content, $code) = fetch($protocol . '://' . $host, undef(), 1);
  1582. if ($code == 200 && $content =~ m/${match}/i) {
  1583. $search_url = $protocol . '://' . $host . $path;
  1584. last;
  1585. } elsif ($DEBUG) {
  1586. print STDERR 'Proxy not available: ' . $host . "\n";
  1587. if ($DEBUG > 1) {
  1588. print STDERR $content . "\n";
  1589. }
  1590. }
  1591. }
  1592. # Return the search data if at least one proxy is up
  1593. if ($search_url) {
  1594. my %tmp = (
  1595. 'protocol' => $protocol,
  1596. 'host' => $host,
  1597. 'search_url' => $search_url,
  1598. );
  1599. return \%tmp;
  1600. }
  1601. # Return undef if we didn't find any working proxies
  1602. return undef();
  1603. }
  1604. sub fetch($;$$) {
  1605. my ($url, $file, $nocheck) = @_;
  1606. if ($DEBUG) {
  1607. print STDERR 'Fetch: ' . $url . "\n";
  1608. }
  1609. # Allow usage with short names
  1610. if ($file && !($file =~ /^\//)) {
  1611. $file = '/tmp/findTorrent-' . $file;
  1612. }
  1613. # Random delay
  1614. if ($DEBUG > 1) {
  1615. sleep($SLEEP * 0.25);
  1616. } else {
  1617. sleep($DELAY * (rand($SLEEP) + ($SLEEP / 2)));
  1618. }
  1619. # Allow JS processing
  1620. if ($url =~ /^phantomjs\:\s*(\S.*)$/i) {
  1621. return phantomFetch($1, $file);
  1622. }
  1623. # Fetch object
  1624. if (!$FETCH) {
  1625. if (!$COOKIES) {
  1626. $COOKIES = mktemp('/tmp/findTorrent.cookies.XXXXXXXX');
  1627. }
  1628. $FETCH = Fetch->new(
  1629. 'cookiefile' => $COOKIES,
  1630. 'timeout' => $TIMEOUT,
  1631. 'uas' => $UA
  1632. );
  1633. }
  1634. # Debug as requested
  1635. if ($DEBUG > 1 && $file) {
  1636. $FETCH->file($file);
  1637. } else {
  1638. $FETCH->file('');
  1639. }
  1640. # Standard fetch
  1641. $FETCH->url($url);
  1642. if ($nocheck) {
  1643. $FETCH->fetch('nocheck' => 1);
  1644. } else {
  1645. $FETCH->fetch();
  1646. }
  1647. return (scalar($FETCH->content()), $FETCH->status_code());
  1648. }
  1649. sub phantomFetch($$) {
  1650. my ($url, $file) = @_;
  1651. # Init
  1652. if (!$SCRIPT) {
  1653. $SCRIPT = mktemp('/tmp/findTorrent.phantom.XXXXXXXX');
  1654. open(my $fh, '>', $SCRIPT)
  1655. or die('Unable to open PhantomJS script file: ' . $! . "\n");
  1656. print $fh $PHANTOM_CONFIG;
  1657. close($fh);
  1658. }
  1659. # Capture STDOUT, drop STDERR
  1660. my @cmd = ($PHANTOM, $SCRIPT, $url);
  1661. my ($content, undef(), undef()) = capture { system(@cmd); };
  1662. # Debug as requested
  1663. if ($DEBUG > 1 && $file) {
  1664. open(my $fh, '>', $file)
  1665. or warn('Unable to open phantom debug file: ' . $! . "\n");
  1666. if ($fh) {
  1667. print $fh $content;
  1668. close($fh);
  1669. }
  1670. }
  1671. # Try to get the response status code, fake one if we cannot
  1672. my ($code) = $content =~ /^(\d\d\d)$/m;
  1673. if (!defined($code)) {
  1674. warn("Unable to retrive HTTP status code. Faking success...\n");
  1675. $code = 200;
  1676. }
  1677. return ($content, $code);
  1678. }