Oppaitime's version of Gazelle
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

torrentsearch.class.php 22KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670
  1. <?
  2. class TorrentSearch {
  3. const TAGS_ANY = 0;
  4. const TAGS_ALL = 1;
  5. const SPH_BOOL_AND = ' ';
  6. const SPH_BOOL_OR = ' | ';
  7. // Map of sort mode => attribute name for ungrouped torrent page
  8. public static $SortOrders = array(
  9. 'year' => 'year',
  10. 'time' => 'id',
  11. 'size' => 'size',
  12. 'seeders' => 'seeders',
  13. 'leechers' => 'leechers',
  14. 'snatched' => 'snatched',
  15. 'random' => 1);
  16. // Map of sort mode => attribute name for grouped torrent page
  17. private static $SortOrdersGrouped = array(
  18. 'year' => 'year',
  19. 'time' => 'id',
  20. 'size' => 'maxsize',
  21. 'seeders' => 'sumseeders',
  22. 'leechers' => 'sumleechers',
  23. 'snatched' => 'sumsnatched',
  24. 'random' => 1);
  25. // Map of sort mode => aggregate expression required for some grouped sort orders
  26. private static $AggregateExp = array(
  27. 'size' => 'MAX(size) AS maxsize',
  28. 'seeders' => 'SUM(seeders) AS sumseeders',
  29. 'leechers' => 'SUM(leechers) AS sumleechers',
  30. 'snatched' => 'SUM(snatched) AS sumsnatched');
  31. // Map of attribute name => global variable name with list of values that can be used for filtering
  32. private static $Attributes = array(
  33. 'filter_cat' => false,
  34. 'releasetype' => 'ReleaseTypes',
  35. 'freetorrent' => false,
  36. 'censored' => false,
  37. 'year' => false);
  38. // List of fields that can be used for fulltext searches
  39. private static $Fields = array(
  40. 'artistname' => 1,
  41. 'audioformat' => 1,
  42. 'cataloguenumber' => 1,
  43. 'codec' => 1,
  44. 'container' => 1,
  45. 'description' => 1,
  46. 'dlsiteid' => 1,
  47. 'filelist' => 1,
  48. 'groupname' => 1,
  49. 'groupnamerj' => 1,
  50. 'groupnamejp' => 1,
  51. 'advgroupname' => 1,
  52. 'language' => 1,
  53. 'media' => 1,
  54. 'resolution' => 1,
  55. 'searchstr' => 1,
  56. 'series' => 1,
  57. 'studio' => 1,
  58. 'subber' => 1,
  59. 'subbing' => 1,
  60. 'taglist' => 1);
  61. // List of torrent-specific fields that can be used for filtering
  62. private static $TorrentFields = array(
  63. 'description' => 1,
  64. 'encoding' => 1,
  65. 'censored' => 1,
  66. 'language' => 1,
  67. 'filelist' => 1,
  68. 'format' => 1,
  69. 'media' => 1);
  70. // Some form field names don't match the ones in the index
  71. private static $FormsToFields = array(
  72. 'searchstr' => '(groupname,groupnamerj,groupnamejp,artistname,studio,series,dlsiteid,cataloguenumber,yearfulltext)',
  73. 'advgroupname' => '(groupname,groupnamerj,groupnamejp)');
  74. // Specify the operator type to use for fields. Empty key sets the default
  75. private static $FieldOperators = array(
  76. '' => self::SPH_BOOL_AND,
  77. 'encoding' => self::SPH_BOOL_OR,
  78. 'format' => self::SPH_BOOL_OR,
  79. 'media' => self::SPH_BOOL_OR);
  80. // Specify the separator character to use for fields. Empty key sets the default
  81. private static $FieldSeparators = array(
  82. '' => ' ',
  83. 'encoding' => '|',
  84. 'format' => '|',
  85. 'media' => '|',
  86. 'taglist' => ',');
  87. // Primary SphinxqlQuery object used to get group IDs or torrent IDs for ungrouped searches
  88. private $SphQL;
  89. // Second SphinxqlQuery object used to get torrent IDs if torrent-specific fulltext filters are used
  90. private $SphQLTor;
  91. // Ordered result array or false if query resulted in an error
  92. private $SphResults;
  93. // Requested page
  94. private $Page;
  95. // Number of results per page
  96. private $PageSize;
  97. // Number of results
  98. private $NumResults = 0;
  99. // Array with info from all matching torrent groups
  100. private $Groups = array();
  101. // Whether any filters were used
  102. private $Filtered = false;
  103. // Whether the random sort order is selected
  104. private $Random = false;
  105. /*
  106. * Storage for fulltext search terms
  107. * ['Field name' => [
  108. * 'include' => [],
  109. * 'exclude' => [],
  110. * 'operator' => self::SPH_BOOL_AND | self::SPH_BOOL_OR
  111. * ]], ...
  112. */
  113. private $Terms = array();
  114. // Unprocessed search terms for retrieval
  115. private $RawTerms = array();
  116. // Storage for used torrent-specific attribute filters
  117. // ['Field name' => 'Search expression', ...]
  118. private $UsedTorrentAttrs = array();
  119. // Storage for used torrent-specific fulltext fields
  120. // ['Field name' => 'Search expression', ...]
  121. private $UsedTorrentFields = array();
  122. /**
  123. * Initialize and configure a TorrentSearch object
  124. *
  125. * @param bool $GroupResults whether results should be grouped by group id
  126. * @param string $OrderBy attribute to use for sorting the results
  127. * @param string $OrderWay Whether to use ascending or descending order
  128. * @param int $Page Page number to display
  129. * @param int $PageSize Number of results per page
  130. */
  131. public function __construct($GroupResults, $OrderBy, $OrderWay, $Page, $PageSize) {
  132. if ($GroupResults && !isset(self::$SortOrdersGrouped[$OrderBy])
  133. || !$GroupResults && !isset(self::$SortOrders[$OrderBy])
  134. || !in_array($OrderWay, array('asc', 'desc'))
  135. ) {
  136. global $Debug;
  137. $ErrMsg = "TorrentSearch constructor arguments:\n" . print_r(func_get_args(), true);
  138. $Debug->analysis('Bad arguments in TorrentSearch constructor', $ErrMsg, 3600*24);
  139. error('-1');
  140. }
  141. if (!is_number($Page) || $Page < 1) {
  142. $Page = 1;
  143. }
  144. if (check_perms('site_search_many')) {
  145. $this->Page = $Page;
  146. } else {
  147. $this->Page = min($Page, SPHINX_MAX_MATCHES / $PageSize);
  148. }
  149. $ResultLimit = $PageSize;
  150. $this->PageSize = $PageSize;
  151. $this->GroupResults = $GroupResults;
  152. $this->SphQL = new SphinxqlQuery();
  153. $this->SphQL->where_match('_all', 'fake', false);
  154. if ($OrderBy === 'random') {
  155. $this->SphQL->select('id, groupid')
  156. ->order_by('RAND()', '');
  157. $this->Random = true;
  158. $this->Page = 1;
  159. if ($GroupResults) {
  160. // Get more results because ORDER BY RAND() can't be used in GROUP BY queries
  161. $ResultLimit *= 5;
  162. }
  163. } elseif ($GroupResults) {
  164. $Select = 'groupid';
  165. if (isset(self::$AggregateExp[$OrderBy])) {
  166. $Select .= ', ' . self::$AggregateExp[$OrderBy];
  167. }
  168. $this->SphQL->select($Select)
  169. ->group_by('groupid')
  170. ->order_group_by(self::$SortOrdersGrouped[$OrderBy], $OrderWay)
  171. ->order_by(self::$SortOrdersGrouped[$OrderBy], $OrderWay);
  172. } else {
  173. $this->SphQL->select('id, groupid')
  174. ->order_by(self::$SortOrders[$OrderBy], $OrderWay);
  175. }
  176. $Offset = ($this->Page - 1) * $ResultLimit;
  177. $MinMax = G::$Cache->get_value('sphinx_min_max_matches');
  178. $MaxMatches = max($Offset + $ResultLimit, $MinMax ? $MinMax : 2000);
  179. $this->SphQL->from('torrents, delta')
  180. ->limit($Offset, $ResultLimit, $MaxMatches);
  181. }
  182. /**
  183. * Process search terms and run the main query
  184. *
  185. * @param array $Terms Array containing all search terms (e.g. $_GET)
  186. * @return array List of matching group IDs with torrent ID as key for ungrouped results
  187. */
  188. public function query($Terms = array()) {
  189. $this->process_search_terms($Terms);
  190. $this->build_query();
  191. $this->run_query();
  192. $this->process_results();
  193. return $this->SphResults;
  194. }
  195. public function insert_hidden_tags($tags) {
  196. $this->SphQL->where_match($tags, 'taglist', false);
  197. }
  198. /**
  199. * Internal function that runs the queries needed to get the desired results
  200. */
  201. private function run_query() {
  202. $SphQLResult = $this->SphQL->query();
  203. if ($SphQLResult->Errno > 0) {
  204. $this->SphResults = false;
  205. return;
  206. }
  207. if ($this->Random && $this->GroupResults) {
  208. $TotalCount = $SphQLResult->get_meta('total_found');
  209. $this->SphResults = $SphQLResult->collect('groupid');
  210. $GroupIDs = array_keys($this->SphResults);
  211. $GroupCount = count($GroupIDs);
  212. while ($SphQLResult->get_meta('total') < $TotalCount && $GroupCount < $this->PageSize) {
  213. // Make sure we get $PageSize results, or all of them if there are less than $PageSize hits
  214. $this->SphQL->where('groupid', $GroupIDs, true);
  215. $SphQLResult = $this->SphQL->query();
  216. if (!$SphQLResult->has_results()) {
  217. break;
  218. }
  219. $this->SphResults += $SphQLResult->collect('groupid');
  220. $GroupIDs = array_keys($this->SphResults);
  221. $GroupCount = count($GroupIDs);
  222. }
  223. if ($GroupCount > $this->PageSize) {
  224. $this->SphResults = array_slice($this->SphResults, 0, $this->PageSize, true);
  225. }
  226. $this->NumResults = count($this->SphResults);
  227. } else {
  228. $this->NumResults = (int)$SphQLResult->get_meta('total_found');
  229. if ($this->GroupResults) {
  230. $this->SphResults = $SphQLResult->collect('groupid');
  231. } else {
  232. $this->SphResults = $SphQLResult->to_pair('id', 'groupid');
  233. }
  234. }
  235. }
  236. /**
  237. * Process search terms and store the parts in appropriate arrays until we know if
  238. * the NOT operator can be used
  239. */
  240. private function build_query() {
  241. foreach ($this->Terms as $Field => $Words) {
  242. $SearchString = '';
  243. if (isset(self::$FormsToFields[$Field])) {
  244. $Field = self::$FormsToFields[$Field];
  245. }
  246. $QueryParts = array('include' => array(), 'exclude' => array());
  247. if (!empty($Words['include'])) {
  248. foreach ($Words['include'] as $Word) {
  249. $QueryParts['include'][] = Sphinxql::sph_escape_string($Word);
  250. }
  251. }
  252. if (!empty($Words['exclude'])) {
  253. foreach ($Words['exclude'] as $Word) {
  254. $QueryParts['exclude'][] = '!' . Sphinxql::sph_escape_string(substr($Word, 1));
  255. }
  256. }
  257. if (!empty($QueryParts)) {
  258. if (isset($Words['operator'])) {
  259. // Is the operator already specified?
  260. $Operator = $Words['operator'];
  261. } elseif(isset(self::$FieldOperators[$Field])) {
  262. // Does this field have a non-standard operator?
  263. $Operator = self::$FieldOperators[$Field];
  264. } else {
  265. // Go for the default operator
  266. $Operator = self::$FieldOperators[''];
  267. }
  268. if (!empty($QueryParts['include'])) {
  269. if ($Field == 'taglist') {
  270. foreach ($QueryParts['include'] as $key => $Tag) {
  271. $QueryParts['include'][$key] = '( '.$Tag.' | '.$Tag.':* )';
  272. }
  273. }
  274. $SearchString .= '( ' . implode($Operator, $QueryParts['include']) . ' ) ';
  275. }
  276. if (!empty($QueryParts['exclude'])) {
  277. $SearchString .= implode(' ', $QueryParts['exclude']);
  278. }
  279. $this->SphQL->where_match($SearchString, $Field, false);
  280. if (isset(self::$TorrentFields[$Field])) {
  281. $this->UsedTorrentFields[$Field] = $SearchString;
  282. }
  283. $this->Filtered = true;
  284. }
  285. }
  286. }
  287. /**
  288. * Look at each search term and figure out what to do with it
  289. *
  290. * @param array $Terms Array with search terms from query()
  291. */
  292. private function process_search_terms($Terms) {
  293. foreach ($Terms as $Key => $Term) {
  294. if (isset(self::$Fields[$Key])) {
  295. $this->process_field($Key, $Term);
  296. } elseif (isset(self::$Attributes[$Key])) {
  297. $this->process_attribute($Key, $Term);
  298. }
  299. $this->RawTerms[$Key] = $Term;
  300. }
  301. $this->post_process_fields();
  302. }
  303. /**
  304. * Process attribute filters and store them in case we need to post-process grouped results
  305. *
  306. * @param string $Attribute Name of the attribute to filter against
  307. * @param mixed $Value The filter's condition for a match
  308. */
  309. private function process_attribute($Attribute, $Value) {
  310. if ($Value === '') {
  311. return;
  312. }
  313. switch ($Attribute) {
  314. case 'year':
  315. if (!$this->search_year($Value)) {
  316. return;
  317. }
  318. break;
  319. case 'freetorrent':
  320. if ($Value == 3) {
  321. $this->SphQL->where('freetorrent', 0, true);
  322. $this->UsedTorrentAttrs['freetorrent'] = 3;
  323. } elseif ($Value >= 0 && $Value < 3) {
  324. $this->SphQL->where('freetorrent', $Value);
  325. $this->UsedTorrentAttrs[$Attribute] = $Value;
  326. } else {
  327. return;
  328. }
  329. break;
  330. case 'filter_cat':
  331. if (!is_array($Value)) {
  332. $Value = array_fill_keys(explode('|', $Value), 1);
  333. }
  334. $CategoryFilter = array();
  335. foreach (array_keys($Value) as $Category) {
  336. if (is_number($Category)) {
  337. $CategoryFilter[] = $Category;
  338. } else {
  339. global $Categories;
  340. $ValidValues = array_map('strtolower', $Categories);
  341. if (($CategoryID = array_search(strtolower($Category), $ValidValues)) !== false) {
  342. $CategoryFilter[] = $CategoryID + 1;
  343. }
  344. }
  345. }
  346. if (empty($CategoryFilter)) {
  347. $CategoryFilter = 0;
  348. }
  349. $this->SphQL->where('categoryid', $CategoryFilter);
  350. break;
  351. default:
  352. if (!is_number($Value) && self::$Attributes[$Attribute] !== false) {
  353. // Check if the submitted value can be converted to a valid one
  354. $ValidValuesVarname = self::$Attributes[$Attribute];
  355. global $$ValidValuesVarname;
  356. $ValidValues = array_map('strtolower', $$ValidValuesVarname);
  357. if (($Value = array_search(strtolower($Value), $ValidValues)) === false) {
  358. // Force the query to return 0 results if value is still invalid
  359. $Value = max(array_keys($ValidValues)) + 1;
  360. }
  361. }
  362. $this->SphQL->where($Attribute, $Value);
  363. $this->UsedTorrentAttrs[$Attribute] = $Value;
  364. break;
  365. }
  366. $this->Filtered = true;
  367. }
  368. /**
  369. * Look at a fulltext search term and figure out if it needs special treatment
  370. *
  371. * @param string $Field Name of the search field
  372. * @param string $Term Search expression for the field
  373. */
  374. private function process_field($Field, $Term) {
  375. $Term = trim($Term);
  376. if ($Term === '') {
  377. return;
  378. }
  379. if ($Field === 'searchstr') {
  380. $this->search_basic($Term);
  381. } elseif ($Field === 'filelist') {
  382. $this->search_filelist($Term);
  383. } elseif ($Field === 'taglist') {
  384. $this->search_taglist($Term);
  385. } else {
  386. $this->add_field($Field, $Term);
  387. }
  388. }
  389. /**
  390. * Some fields may require post-processing
  391. */
  392. private function post_process_fields() {
  393. if (isset($this->Terms['taglist'])) {
  394. // Replace bad tags with tag aliases
  395. $this->Terms['taglist'] = Tags::remove_aliases($this->Terms['taglist']);
  396. if (isset($this->RawTerms['tags_type']) && (int)$this->RawTerms['tags_type'] === self::TAGS_ANY) {
  397. $this->Terms['taglist']['operator'] = self::SPH_BOOL_OR;
  398. }
  399. // Update the RawTerms array so get_terms() can return the corrected search terms
  400. if (isset($this->Terms['taglist']['include'])) {
  401. $AllTags = $this->Terms['taglist']['include'];
  402. } else {
  403. $AllTags = array();
  404. }
  405. if (isset($this->Terms['taglist']['exclude'])) {
  406. $AllTags = array_merge($AllTags, $this->Terms['taglist']['exclude']);
  407. }
  408. $this->RawTerms['taglist'] = str_replace('_', '.', implode(', ', $AllTags));
  409. }
  410. }
  411. /**
  412. * Handle magic keywords in the basic torrent search
  413. *
  414. * @param string $Term Given search expression
  415. */
  416. private function search_basic($Term) {
  417. global $Bitrates, $Formats, $Media;
  418. $SearchBitrates = array_map('strtolower', $Bitrates);
  419. array_push($SearchBitrates, 'v0', 'v1', 'v2', '24bit');
  420. $SearchFormats = array_map('strtolower', $Formats);
  421. $SearchMedia = array_map('strtolower', $Media);
  422. foreach (explode(' ', $Term) as $Word) {
  423. if (in_array($Word, $SearchBitrates)) {
  424. $this->add_word('encoding', $Word);
  425. } elseif (in_array($Word, $SearchFormats)) {
  426. $this->add_word('format', $Word);
  427. } elseif (in_array($Word, $SearchMedia)) {
  428. $this->add_word('media', $Word);
  429. } else {
  430. $this->add_word('searchstr', $Word);
  431. }
  432. }
  433. }
  434. /**
  435. * Use phrase boundary for file searches to make sure we don't count
  436. * partial hits from multiple files
  437. *
  438. * @param string $Term Given search expression
  439. */
  440. private function search_filelist($Term) {
  441. $SearchString = '"' . Sphinxql::sph_escape_string($Term) . '"~20';
  442. $this->SphQL->where_match($SearchString, 'filelist', false);
  443. $this->UsedTorrentFields['filelist'] = $SearchString;
  444. $this->Filtered = true;
  445. }
  446. /**
  447. * Prepare tag searches before sending them to the normal treatment
  448. *
  449. * @param string $Term Given search expression
  450. */
  451. private function search_taglist($Term) {
  452. $Term = strtr($Term, '.', '_');
  453. $this->add_field('taglist', $Term);
  454. }
  455. /**
  456. * The year filter accepts a range. Figure out how to handle the filter value
  457. *
  458. * @param string $Term Filter condition. Can be an integer or a range with the format X-Y
  459. * @return bool True if parameters are valid
  460. */
  461. private function search_year($Term) {
  462. $Years = explode('-', $Term);
  463. if (count($Years) === 1 && is_number($Years[0])) {
  464. // Exact year
  465. $this->SphQL->where('year', $Years[0]);
  466. } elseif (count($Years) === 2) {
  467. if (empty($Years[0]) && is_number($Years[1])) {
  468. // Range: 0 - 2005
  469. $this->SphQL->where_lt('year', $Years[1], true);
  470. } elseif (empty($Years[1]) && is_number($Years[0])) {
  471. // Range: 2005 - 2^32-1
  472. $this->SphQL->where_gt('year', $Years[0], true);
  473. } elseif (is_number($Years[0]) && is_number($Years[1])) {
  474. // Range: 2005 - 2009
  475. $this->SphQL->where_between('year', array(min($Years), max($Years)));
  476. } else {
  477. // Invalid input
  478. return false;
  479. }
  480. } else {
  481. // Invalid input
  482. return false;
  483. }
  484. return true;
  485. }
  486. /**
  487. * Add a field filter that doesn't need special treatment
  488. *
  489. * @param string $Field Name of the search field
  490. * @param string $Term Search expression for the field
  491. */
  492. private function add_field($Field, $Term) {
  493. if (isset(self::$FieldSeparators[$Field])) {
  494. $Separator = self::$FieldSeparators[$Field];
  495. } else {
  496. $Separator = self::$FieldSeparators[''];
  497. }
  498. $Words = explode($Separator, $Term);
  499. foreach ($Words as $Word) {
  500. $this->add_word($Field, $Word);
  501. }
  502. }
  503. /**
  504. * Add a keyword to the array of search terms
  505. *
  506. * @param string $Field Name of the search field
  507. * @param string $Word Keyword
  508. */
  509. private function add_word($Field, $Word) {
  510. $Word = trim($Word);
  511. // Skip isolated hyphens to enable "Artist - Title" searches
  512. if ($Word === '' || $Word === '-') {
  513. return;
  514. }
  515. if ($Word[0] === '!' && strlen($Word) >= 2 && strpos($Word, '!', 1) === false) {
  516. $this->Terms[$Field]['exclude'][] = $Word;
  517. } else {
  518. $this->Terms[$Field]['include'][] = $Word;
  519. }
  520. }
  521. /**
  522. * @return array Torrent group information for the matches from Torrents::get_groups
  523. */
  524. public function get_groups() {
  525. return $this->Groups;
  526. }
  527. /**
  528. * @param string $Type Field or attribute name
  529. * @return string Unprocessed search terms
  530. */
  531. public function get_terms($Type) {
  532. return $this->RawTerms[$Type] ?? '';
  533. }
  534. /**
  535. * @return int Result count
  536. */
  537. public function record_count() {
  538. return $this->NumResults;
  539. }
  540. /**
  541. * @return bool Whether any filters were used
  542. */
  543. public function has_filters() {
  544. return $this->Filtered;
  545. }
  546. /**
  547. * @return bool Whether any torrent-specific fulltext filters were used
  548. */
  549. public function need_torrent_ft() {
  550. return $this->GroupResults && $this->NumResults > 0 && !empty($this->UsedTorrentFields);
  551. }
  552. /**
  553. * Get torrent group info and remove any torrents that don't match
  554. */
  555. private function process_results() {
  556. if (count($this->SphResults) == 0) {
  557. return;
  558. }
  559. $this->Groups = Torrents::get_groups($this->SphResults);
  560. if ($this->need_torrent_ft()) {
  561. // Query Sphinx for torrent IDs if torrent-specific fulltext filters were used
  562. $this->filter_torrents_sph();
  563. } elseif ($this->GroupResults) {
  564. // Otherwise, let PHP discard unmatching torrents
  565. $this->filter_torrents_internal();
  566. }
  567. // Ungrouped searches don't need any additional filtering
  568. }
  569. /**
  570. * Build and run a query that gets torrent IDs from Sphinx when fulltext filters
  571. * were used to get primary results and they are grouped
  572. */
  573. private function filter_torrents_sph() {
  574. $AllTorrents = array();
  575. foreach ($this->Groups as $GroupID => $Group) {
  576. if (!empty($Group['Torrents'])) {
  577. $AllTorrents += array_fill_keys(array_keys($Group['Torrents']), $GroupID);
  578. }
  579. }
  580. $TorrentCount = count($AllTorrents);
  581. $this->SphQLTor = new SphinxqlQuery();
  582. $this->SphQLTor->select('id')->from('torrents, delta');
  583. foreach ($this->UsedTorrentFields as $Field => $Term) {
  584. $this->SphQLTor->where_match($Term, $Field, false);
  585. }
  586. $this->SphQLTor->copy_attributes_from($this->SphQL);
  587. $this->SphQLTor->where('id', array_keys($AllTorrents))->limit(0, $TorrentCount, $TorrentCount);
  588. $SphQLResultTor = $this->SphQLTor->query();
  589. $MatchingTorrentIDs = $SphQLResultTor->to_pair('id', 'id');
  590. foreach ($AllTorrents as $TorrentID => $GroupID) {
  591. if (!isset($MatchingTorrentIDs[$TorrentID])) {
  592. unset($this->Groups[$GroupID]['Torrents'][$TorrentID]);
  593. }
  594. }
  595. }
  596. /**
  597. * Non-Sphinx method of collecting IDs of torrents that match any
  598. * torrent-specific attribute filters that were used in the search query
  599. */
  600. private function filter_torrents_internal() {
  601. foreach ($this->Groups as $GroupID => $Group) {
  602. if (empty($Group['Torrents'])) {
  603. continue;
  604. }
  605. foreach ($Group['Torrents'] as $TorrentID => $Torrent) {
  606. if (!$this->filter_torrent_internal($Torrent)) {
  607. unset($this->Groups[$GroupID]['Torrents'][$TorrentID]);
  608. }
  609. }
  610. }
  611. }
  612. /**
  613. * Post-processing to determine if a torrent is a real hit or if it was
  614. * returned because another torrent in the group matched. Only used if
  615. * there are no torrent-specific fulltext conditions
  616. *
  617. * @param array $Torrent Torrent array, probably from Torrents::get_groups()
  618. * @return bool True if it's a real hit
  619. */
  620. private function filter_torrent_internal($Torrent) {
  621. if (isset($this->UsedTorrentAttrs['freetorrent'])) {
  622. $FilterValue = $this->UsedTorrentAttrs['freetorrent'];
  623. if ($FilterValue == '3' && $Torrent['FreeTorrent'] == '0') {
  624. // Either FL or NL is ok
  625. return false;
  626. } elseif ($FilterValue != '3' && $FilterValue != (int)$Torrent['FreeTorrent']) {
  627. return false;
  628. }
  629. }
  630. return true;
  631. }
  632. }