BioTorrents.de’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.

torrents.class.php 51KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443
  1. <?php
  2. #declare(strict_types=1);
  3. class Torrents
  4. {
  5. const FILELIST_DELIM = 0xF7; // Hex for &divide; Must be the same as phrase_boundary in sphinx.conf!
  6. const SNATCHED_UPDATE_INTERVAL = 3600; // How often we want to update users' snatch lists
  7. const SNATCHED_UPDATE_AFTERDL = 300; // How long after a torrent download we want to update a user's snatch lists
  8. /**
  9. * Function to get data and torrents for an array of GroupIDs. Order of keys doesn't matter
  10. *
  11. * @param array $GroupIDs
  12. * @param boolean $Return if false, nothing is returned. For priming cache.
  13. * @param boolean $GetArtists if true, each group will contain the result of
  14. * Artists::get_artists($GroupID), in result[$GroupID]['ExtendedArtists']
  15. * @param boolean $Torrents if true, each group contains a list of torrents, in result[$GroupID]['Torrents']
  16. *
  17. * @return array each row of the following format:
  18. * GroupID => (
  19. * ID
  20. * Name
  21. * Year
  22. * RecordLabel
  23. * CatalogueNumber
  24. * TagList
  25. * ReleaseType
  26. * VanityHouse
  27. * WikiImage
  28. * CategoryID
  29. * Torrents => {
  30. * ID => {
  31. * GroupID, Media, Format, Encoding, RemasterYear, Remastered,
  32. * RemasterTitle, RemasterRecordLabel, RemasterCatalogueNumber, Scene,
  33. * HasLog, HasCue, LogScore, FileCount, FreeTorrent, Size, Leechers,
  34. * Seeders, Snatched, Time, HasFile, PersonalFL, IsSnatched
  35. * }
  36. * }
  37. * Artists => {
  38. * {
  39. * id, name, aliasid // Only main artists
  40. * }
  41. * }
  42. * ExtendedArtists => {
  43. * [1-6] => { // See documentation on Artists::get_artists
  44. * id, name, aliasid
  45. * }
  46. * }
  47. * Flags => {
  48. * IsSnatched
  49. * }
  50. */
  51. public static function get_groups($GroupIDs, $Return = true, $GetArtists = true, $Torrents = true)
  52. {
  53. $Found = $NotFound = array_fill_keys($GroupIDs, false);
  54. $Key = $Torrents ? 'torrent_group_' : 'torrent_group_light_';
  55. foreach ($GroupIDs as $i => $GroupID) {
  56. if (!is_number($GroupID)) {
  57. unset($GroupIDs[$i], $Found[$GroupID], $NotFound[$GroupID]);
  58. continue;
  59. }
  60. $Data = G::$Cache->get_value($Key . $GroupID, true);
  61. if (!empty($Data) && is_array($Data) && $Data['ver'] === Cache::GROUP_VERSION) {
  62. unset($NotFound[$GroupID]);
  63. $Found[$GroupID] = $Data['d'];
  64. }
  65. }
  66. // Make sure there's something in $GroupIDs, otherwise the SQL will break
  67. if (count($GroupIDs) === 0) {
  68. return [];
  69. }
  70. /**
  71. * Changing any of these attributes returned will cause very large, very dramatic site-wide chaos.
  72. * Do not change what is returned or the order thereof without updating:
  73. * torrents, artists, collages, bookmarks, better, the front page,
  74. * and anywhere else the get_groups function is used.
  75. * Update self::array_group(), too.
  76. */
  77. if (count($NotFound) > 0) {
  78. $IDs = implode(',', array_keys($NotFound));
  79. $NotFound = [];
  80. $QueryID = G::$DB->get_query_id();
  81. G::$DB->prepare_query("
  82. SELECT
  83. `id`,
  84. `title`,
  85. `subject`,
  86. `object`,
  87. `year`,
  88. `identifier`,
  89. `workgroup`,
  90. `location`,
  91. `tag_list`,
  92. `picture`,
  93. `category_id`
  94. FROM
  95. `torrents_group`
  96. WHERE
  97. `id` IN($IDs)
  98. ");
  99. G::$DB->exec_prepared_query();
  100. while ($Group = G::$DB->next_record(MYSQLI_ASSOC, true)) {
  101. $NotFound[$Group['id']] = $Group;
  102. $NotFound[$Group['id']]['Torrents'] = [];
  103. $NotFound[$Group['id']]['Artists'] = [];
  104. }
  105. G::$DB->set_query_id($QueryID);
  106. if ($Torrents) {
  107. $QueryID = G::$DB->get_query_id();
  108. G::$DB->query("
  109. SELECT
  110. `ID`,
  111. `GroupID`,
  112. `Media`,
  113. `Container`,
  114. `Codec`,
  115. `Resolution`,
  116. `Version`,
  117. `Censored`,
  118. `Archive`,
  119. `FileCount`,
  120. `FreeTorrent`,
  121. `Size`,
  122. `Leechers`,
  123. `Seeders`,
  124. `Snatched`,
  125. `Time`,
  126. f.`ExpiryTime`,
  127. `ID` AS `HasFile`,
  128. `FreeLeechType`,
  129. HEX(`info_hash`) AS `info_hash`
  130. FROM
  131. `torrents`
  132. LEFT JOIN `shop_freeleeches` AS f
  133. ON
  134. f.`TorrentID` = `ID`
  135. WHERE
  136. `GroupID` IN($IDs)
  137. ORDER BY
  138. `GroupID`,
  139. `Media`,
  140. `Container`,
  141. `Codec`,
  142. `ID`
  143. ");
  144. while ($Torrent = G::$DB->next_record(MYSQLI_ASSOC, true)) {
  145. $NotFound[$Torrent['GroupID']]['Torrents'][$Torrent['ID']] = $Torrent;
  146. }
  147. G::$DB->set_query_id($QueryID);
  148. }
  149. foreach ($NotFound as $GroupID => $GroupInfo) {
  150. G::$Cache->cache_value($Key . $GroupID, array('ver' => Cache::GROUP_VERSION, 'd' => $GroupInfo), 0);
  151. }
  152. $Found = $NotFound + $Found;
  153. }
  154. // Filter out orphans (elements that are === false)
  155. $Found = array_filter($Found);
  156. if ($GetArtists) {
  157. $Artists = Artists::get_artists($GroupIDs);
  158. } else {
  159. $Artists = [];
  160. }
  161. if ($Return) { // If we're interested in the data, and not just caching it
  162. foreach ($Artists as $GroupID => $Data) {
  163. if (!isset($Found[$GroupID])) {
  164. continue;
  165. }
  166. $Found[$GroupID]['Artists'] = $Data;
  167. }
  168. // Fetch all user specific torrent properties
  169. if ($Torrents) {
  170. foreach ($Found as &$Group) {
  171. $Group['Flags'] = array('IsSnatched' => false, 'IsSeeding' => false, 'IsLeeching' => false);
  172. if (!empty($Group['Torrents'])) {
  173. foreach ($Group['Torrents'] as &$Torrent) {
  174. self::torrent_properties($Torrent, $Group['Flags']);
  175. }
  176. }
  177. }
  178. }
  179. return $Found;
  180. }
  181. }
  182. /**
  183. * Returns a reconfigured array from a Torrent Group
  184. *
  185. * Use this with extract() instead of the volatile list($GroupID, ...)
  186. * Then use the variables $GroupID, $GroupName, etc
  187. *
  188. * @example extract(Torrents::array_group($SomeGroup));
  189. * @param array $Group torrent group
  190. * @return array Re-key'd array
  191. */
  192. public static function array_group(array &$Group)
  193. {
  194. return array(
  195. 'id' => $Group['id'],
  196. 'title' => $Group['title'],
  197. 'subject' => $Group['subject'],
  198. 'object' => $Group['object'],
  199. 'published' => $Group['published'],
  200. 'category_id' => $Group['category_id'],
  201. 'identifier' => $Group['identifier'],
  202. 'workgroup' => $Group['workgroup'],
  203. 'location' => $Group['location'],
  204. 'GroupFlags' => ($Group['Flags'] ?? ''),
  205. 'tag_list' => $Group['tag_list'],
  206. 'picture' => $Group['picture'],
  207. 'Torrents' => $Group['Torrents'],
  208. 'Artists' => $Group['Artists']
  209. );
  210. }
  211. /**
  212. * Supplements a torrent array with information that only concerns certain users and therefore cannot be cached
  213. *
  214. * @param array $Torrent torrent array preferably in the form used by Torrents::get_groups() or get_group_info()
  215. * @param int $TorrentID
  216. */
  217. public static function torrent_properties(&$Torrent, &$Flags)
  218. {
  219. # FL Token
  220. $Torrent['PersonalFL'] = empty($Torrent['FreeTorrent']) && self::has_token($Torrent['ID']);
  221. # Snatched
  222. if ($Torrent['IsSnatched'] = self::has_snatched($Torrent['ID'])) {
  223. $Flags['IsSnatched'] = true;
  224. } else {
  225. $Flags['IsSnatched'] = false;
  226. }
  227. # Seeding
  228. if ($Torrent['IsSeeding'] = self::is_seeding($Torrent['ID'])) {
  229. $Flags['IsSeeding'] = true;
  230. } else {
  231. $Flags['IsSeeding'] = false;
  232. }
  233. # Leeching
  234. if ($Torrent['IsLeeching'] = self::is_leeching($Torrent['ID'])) {
  235. $Flags['IsLeeching'] = true;
  236. } else {
  237. $Flags['IsLeeching'] = false;
  238. }
  239. }
  240. /*
  241. * Write to the group log.
  242. *
  243. * @param int $GroupID
  244. * @param int $TorrentID
  245. * @param int $UserID
  246. * @param string $Message
  247. * @param boolean $Hidden Currently does fuck all.
  248. *
  249. * todo: Fix that
  250. */
  251. public static function write_group_log($GroupID, $TorrentID, $UserID, $Message, $Hidden)
  252. {
  253. global $Time;
  254. $QueryID = G::$DB->get_query_id();
  255. G::$DB->query("
  256. INSERT INTO `group_log`(
  257. `GroupID`,
  258. `TorrentID`,
  259. `UserID`,
  260. `Info`,
  261. `Time`,
  262. `Hidden`
  263. )
  264. VALUES(
  265. '$GroupID',
  266. '$TorrentID',
  267. '$UserID',
  268. '$Message',
  269. NOW(),
  270. '$Hidden'
  271. )
  272. ");
  273. G::$DB->set_query_id($QueryID);
  274. }
  275. /**
  276. * Delete a torrent.
  277. *
  278. * @param int $ID The ID of the torrent to delete.
  279. * @param int $GroupID Set it if you have it handy, to save a query. Otherwise, it will be found.
  280. * @param string $OcelotReason The deletion reason for ocelot to report to users.
  281. */
  282. public static function delete_torrent($ID, $GroupID = 0, $OcelotReason = -1)
  283. {
  284. $QueryID = G::$DB->get_query_id();
  285. if (!$GroupID) {
  286. G::$DB->query("
  287. SELECT GroupID, UserID
  288. FROM torrents
  289. WHERE ID = '$ID'");
  290. list($GroupID, $UploaderID) = G::$DB->next_record();
  291. }
  292. if (empty($UserID)) {
  293. G::$DB->query("
  294. SELECT UserID
  295. FROM torrents
  296. WHERE ID = '$ID'");
  297. list($UserID) = G::$DB->next_record();
  298. }
  299. $RecentUploads = G::$Cache->get_value("recent_uploads_$UserID");
  300. if (is_array($RecentUploads)) {
  301. foreach ($RecentUploads as $Key => $Recent) {
  302. if ($Recent['ID'] == $GroupID) {
  303. G::$Cache->delete_value("recent_uploads_$UserID");
  304. }
  305. }
  306. }
  307. G::$DB->query("
  308. SELECT info_hash
  309. FROM torrents
  310. WHERE ID = $ID");
  311. list($InfoHash) = G::$DB->next_record(MYSQLI_BOTH, false);
  312. G::$DB->query("
  313. DELETE FROM torrents
  314. WHERE ID = $ID");
  315. Tracker::update_tracker('delete_torrent', array('info_hash' => rawurlencode($InfoHash), 'id' => $ID, 'reason' => $OcelotReason));
  316. G::$Cache->decrement('stats_torrent_count');
  317. G::$DB->query("
  318. SELECT COUNT(ID)
  319. FROM torrents
  320. WHERE GroupID = '$GroupID'");
  321. list($Count) = G::$DB->next_record();
  322. if ($Count == 0) {
  323. Torrents::delete_group($GroupID);
  324. } else {
  325. Torrents::update_hash($GroupID);
  326. }
  327. // Torrent notifications
  328. G::$DB->query("
  329. SELECT UserID
  330. FROM users_notify_torrents
  331. WHERE TorrentID = '$ID'");
  332. while (list($UserID) = G::$DB->next_record()) {
  333. G::$Cache->delete_value("notifications_new_$UserID");
  334. }
  335. G::$DB->query("
  336. DELETE FROM users_notify_torrents
  337. WHERE TorrentID = '$ID'");
  338. G::$DB->query("
  339. UPDATE reportsv2
  340. SET
  341. Status = 'Resolved',
  342. LastChangeTime = NOW(),
  343. ModComment = 'Report already dealt with (torrent deleted)'
  344. WHERE TorrentID = ?
  345. AND Status != 'Resolved'", $ID);
  346. $Reports = G::$DB->affected_rows();
  347. if ($Reports) {
  348. G::$Cache->decrement('num_torrent_reportsv2', $Reports);
  349. }
  350. unlink(TORRENT_STORE.$ID.'.torrent');
  351. G::$DB->query("
  352. DELETE FROM torrents_bad_tags
  353. WHERE TorrentID = ?", $ID);
  354. G::$DB->query("
  355. DELETE FROM torrents_bad_folders
  356. WHERE TorrentID = ?", $ID);
  357. G::$DB->query("
  358. DELETE FROM torrents_bad_files
  359. WHERE TorrentID = ?", $ID);
  360. G::$DB->query("
  361. DELETE FROM shop_freeleeches
  362. WHERE TorrentID = ?", $ID);
  363. $FLs = G::$DB->affected_rows();
  364. if ($FLs) {
  365. G::$Cache->delete_value('shop_freeleech_list');
  366. }
  367. // Tells Sphinx that the group is removed
  368. G::$DB->query("
  369. REPLACE INTO sphinx_delta (ID, Time)
  370. VALUES (?, UNIX_TIMESTAMP())", $ID);
  371. G::$Cache->delete_value("torrent_download_$ID");
  372. G::$Cache->delete_value("torrent_group_$GroupID");
  373. G::$Cache->delete_value("torrents_details_$GroupID");
  374. G::$DB->set_query_id($QueryID);
  375. }
  376. /**
  377. * Delete a group, called after all of its torrents have been deleted.
  378. * IMPORTANT: Never call this unless you're certain the group is no longer used by any torrents
  379. *
  380. * @param int $GroupID
  381. */
  382. public static function delete_group($GroupID)
  383. {
  384. $QueryID = G::$DB->get_query_id();
  385. Misc::write_log("Group $GroupID automatically deleted (No torrents have this group).");
  386. G::$DB->prepare_query("
  387. SELECT
  388. `category_id`
  389. FROM
  390. `torrents_group`
  391. WHERE
  392. `id` = '$GroupID'
  393. ");
  394. G::$DB->exec_prepared_query();
  395. list($Category) = G::$DB->next_record();
  396. # todo: Check strict equality here
  397. if ($Category === 1) {
  398. G::$Cache->decrement('stats_album_count');
  399. }
  400. G::$Cache->decrement('stats_group_count');
  401. // Collages
  402. G::$DB->query("
  403. SELECT CollageID
  404. FROM collages_torrents
  405. WHERE GroupID = ?", $GroupID);
  406. if (G::$DB->has_results()) {
  407. $CollageIDs = G::$DB->collect('CollageID');
  408. G::$DB->query("
  409. UPDATE collages
  410. SET NumTorrents = NumTorrents - 1
  411. WHERE ID IN (".implode(', ', $CollageIDs).')');
  412. G::$DB->query("
  413. DELETE FROM collages_torrents
  414. WHERE GroupID = ?", $GroupID);
  415. foreach ($CollageIDs as $CollageID) {
  416. G::$Cache->delete_value("collage_$CollageID");
  417. }
  418. G::$Cache->delete_value("torrent_collages_$GroupID");
  419. }
  420. // Artists
  421. // Collect the artist IDs and then wipe the torrents_artist entry
  422. G::$DB->query("
  423. SELECT ArtistID
  424. FROM torrents_artists
  425. WHERE GroupID = ?", $GroupID);
  426. $Artists = G::$DB->collect('ArtistID');
  427. G::$DB->query("
  428. DELETE FROM torrents_artists
  429. WHERE GroupID = ?", $GroupID);
  430. foreach ($Artists as $ArtistID) {
  431. if (empty($ArtistID)) {
  432. continue;
  433. }
  434. // Get a count of how many groups or requests use the artist ID
  435. G::$DB->query("
  436. SELECT COUNT(ag.ArtistID)
  437. FROM artists_group AS ag
  438. LEFT JOIN requests_artists AS ra ON ag.ArtistID = ra.ArtistID
  439. WHERE ra.ArtistID IS NOT NULL
  440. AND ag.ArtistID = ?", $ArtistID);
  441. list($ReqCount) = G::$DB->next_record();
  442. G::$DB->query("
  443. SELECT COUNT(ag.ArtistID)
  444. FROM artists_group AS ag
  445. LEFT JOIN torrents_artists AS ta ON ag.ArtistID = ta.ArtistID
  446. WHERE ta.ArtistID IS NOT NULL
  447. AND ag.ArtistID = ?", $ArtistID);
  448. list($GroupCount) = G::$DB->next_record();
  449. if (($ReqCount + $GroupCount) == 0) {
  450. //The only group to use this artist
  451. Artists::delete_artist($ArtistID);
  452. } else {
  453. //Not the only group, still need to clear cache
  454. G::$Cache->delete_value("artist_groups_$ArtistID");
  455. }
  456. }
  457. // Requests
  458. G::$DB->query("
  459. SELECT ID
  460. FROM requests
  461. WHERE GroupID = ?", $GroupID);
  462. $Requests = G::$DB->collect('ID');
  463. G::$DB->query("
  464. UPDATE requests
  465. SET GroupID = NULL
  466. WHERE GroupID = ?", $GroupID);
  467. foreach ($Requests as $RequestID) {
  468. G::$Cache->delete_value("request_$RequestID");
  469. }
  470. // Comments
  471. Comments::delete_page('torrents', $GroupID);
  472. G::$DB->prepare_query("
  473. DELETE
  474. FROM
  475. `torrents_group`
  476. WHERE
  477. `id` = '$GroupID'
  478. ");
  479. G::$DB->exec_prepared_query();
  480. G::$DB->prepare_query("
  481. DELETE
  482. FROM
  483. `torrents_tags`
  484. WHERE
  485. `GroupID` = '$GroupID'
  486. ");
  487. G::$DB->exec_prepared_query();
  488. G::$DB->prepare_query("
  489. DELETE
  490. FROM
  491. `bookmarks_torrents`
  492. WHERE
  493. `GroupID` = '$GroupID'
  494. ");
  495. G::$DB->exec_prepared_query();
  496. G::$DB->prepare_query("
  497. DELETE
  498. FROM
  499. `wiki_torrents`
  500. WHERE
  501. `PageID` = '$GroupID'
  502. ");
  503. G::$DB->exec_prepared_query();
  504. G::$Cache->delete_value("torrents_details_$GroupID");
  505. G::$Cache->delete_value("torrent_group_$GroupID");
  506. G::$Cache->delete_value("groups_artists_$GroupID");
  507. G::$DB->set_query_id($QueryID);
  508. }
  509. /**
  510. * Update the cache and sphinx delta index to keep everything up-to-date.
  511. *
  512. * @param int $GroupID
  513. */
  514. public static function update_hash(int $GroupID)
  515. {
  516. $QueryID = G::$DB->get_query_id();
  517. G::$DB->prepare_query("
  518. UPDATE
  519. `torrents_group`
  520. SET
  521. `tag_list` =(
  522. SELECT
  523. REPLACE
  524. (
  525. GROUP_CONCAT(tags.Name SEPARATOR ' '),
  526. '.',
  527. '_'
  528. )
  529. FROM
  530. `torrents_tags` AS t
  531. INNER JOIN `tags` ON tags.`ID` = t.`TagID`
  532. WHERE
  533. t.`GroupID` = '$GroupID'
  534. GROUP BY
  535. t.`GroupID`
  536. )
  537. WHERE
  538. `ID` = '$GroupID'
  539. ");
  540. G::$DB->exec_prepared_query();
  541. // Fetch album artists
  542. G::$DB->prepare_query("
  543. SELECT GROUP_CONCAT(ag.`Name` separator ' ')
  544. FROM `torrents_artists` AS `ta`
  545. JOIN `artists_group` AS ag ON ag.`ArtistID` = ta.`ArtistID`
  546. WHERE ta.`GroupID` = '$GroupID'
  547. GROUP BY ta.`GroupID`
  548. ");
  549. G::$DB->exec_prepared_query();
  550. if (G::$DB->has_results()) {
  551. list($ArtistName) = G::$DB->next_record(MYSQLI_NUM, false);
  552. } else {
  553. $ArtistName = '';
  554. }
  555. G::$DB->prepare_query("
  556. REPLACE
  557. INTO sphinx_delta(
  558. `ID`,
  559. `GroupID`,
  560. `GroupName`,
  561. `GroupTitle2`,
  562. `GroupNameJP`,
  563. `TagList`,
  564. `Year`,
  565. `CatalogueNumber`,
  566. `CategoryID`,
  567. `Time`,
  568. `Size`,
  569. `Snatched`,
  570. `Seeders`,
  571. `Leechers`,
  572. `Censored`,
  573. `Studio`,
  574. `Series`,
  575. `FreeTorrent`,
  576. `Media`,
  577. `Container`,
  578. `Codec`,
  579. `Resolution`,
  580. `Version`,
  581. `Description`,
  582. `FileList`,
  583. `ArtistName`
  584. )
  585. SELECT
  586. t.`ID`,
  587. g.`id`,
  588. g.`title`,
  589. g.`subject`,
  590. g.`object`,
  591. g.`tag_list`,
  592. g.`published`,
  593. g.`identifier`,
  594. g.`category_id`,
  595. UNIX_TIMESTAMP(t.`Time`),
  596. t.`Size`,
  597. t.`Snatched`,
  598. t.`Seeders`,
  599. t.`Leechers`,
  600. t.`Censored`,
  601. g.`workgroup`,
  602. g.`location`,
  603. CAST(`FreeTorrent` AS CHAR),
  604. t.`Media`,
  605. t.`Container`,
  606. t.`Codec`,
  607. t.`Resolution`,
  608. t.`Version`,
  609. t.`Description`,
  610. REPLACE
  611. (
  612. REPLACE
  613. (`FileList`, '_', ' '),
  614. '/',
  615. ' '
  616. ) AS FileList,
  617. '$ArtistName'
  618. FROM
  619. `torrents` AS t
  620. JOIN `torrents_group` AS g
  621. ON
  622. g.`id` = t.`GroupID`
  623. WHERE
  624. g.`id` = '$GroupID'
  625. ");
  626. G::$DB->exec_prepared_query();
  627. G::$Cache->delete_value("torrents_details_$GroupID");
  628. G::$Cache->delete_value("torrent_group_$GroupID");
  629. G::$Cache->delete_value("torrent_group_light_$GroupID");
  630. $ArtistInfo = Artists::get_artist($GroupID);
  631. G::$Cache->delete_value("groups_artists_$GroupID");
  632. G::$DB->set_query_id($QueryID);
  633. }
  634. /**
  635. * Regenerate a torrent's file list from its meta data,
  636. * update the database record and clear relevant cache keys
  637. *
  638. * @param int $TorrentID
  639. */
  640. public static function regenerate_filelist($TorrentID)
  641. {
  642. $QueryID = G::$DB->get_query_id();
  643. G::$DB->query("
  644. SELECT GroupID
  645. FROM torrents
  646. WHERE ID = ?", $TorrentID);
  647. if (G::$DB->has_results()) {
  648. list($GroupID) = G::$DB->next_record(MYSQLI_NUM, false);
  649. $Contents = file_get_contents(TORRENT_STORE.$TorrentID.'.torrent');
  650. if (Misc::is_new_torrent($Contents)) {
  651. $Tor = new BencodeTorrent($Contents);
  652. $FilePath = (isset($Tor->Dec['info']['files']) ? Format::make_utf8($Tor->get_name()) : '');
  653. } else {
  654. $Tor = new TORRENT(unserialize(base64_decode($Contents)), true);
  655. $FilePath = (isset($Tor->Val['info']->Val['files']) ? Format::make_utf8($Tor->get_name()) : '');
  656. }
  657. list($TotalSize, $FileList) = $Tor->file_list();
  658. foreach ($FileList as $File) {
  659. $TmpFileList[] = self::filelist_format_file($File);
  660. }
  661. $FileString = implode("\n", $TmpFileList);
  662. G::$DB->query(
  663. "
  664. UPDATE torrents
  665. SET Size = ?, FilePath = ?, FileList = ?
  666. WHERE ID = ?",
  667. $TotalSize,
  668. $FilePath,
  669. $FileString,
  670. $TorrentID
  671. );
  672. G::$Cache->delete_value("torrents_details_$GroupID");
  673. }
  674. G::$DB->set_query_id($QueryID);
  675. }
  676. /**
  677. * Return UTF-8 encoded string to use as file delimiter in torrent file lists
  678. */
  679. public static function filelist_delim()
  680. {
  681. static $FilelistDelimUTF8;
  682. if (isset($FilelistDelimUTF8)) {
  683. return $FilelistDelimUTF8;
  684. }
  685. return $FilelistDelimUTF8 = utf8_encode(chr(self::FILELIST_DELIM));
  686. }
  687. /**
  688. * Create a string that contains file info in a format that's easy to use for Sphinx
  689. *
  690. * @param array $File (File size, File name)
  691. * @return string with the format .EXT sSIZEs NAME DELIMITER
  692. */
  693. public static function filelist_format_file($File)
  694. {
  695. list($Size, $Name) = $File;
  696. $Name = Format::make_utf8(strtr($Name, "\n\r\t", ' '));
  697. $ExtPos = strrpos($Name, '.');
  698. // Should not be $ExtPos !== false. Extensionless files that start with a . should not get extensions
  699. $Ext = ($ExtPos ? trim(substr($Name, $ExtPos + 1)) : '');
  700. return sprintf("%s s%ds %s %s", ".$Ext", $Size, $Name, self::filelist_delim());
  701. }
  702. /**
  703. * Create a string that contains file info in the old format for the API
  704. *
  705. * @param string $File string with the format .EXT sSIZEs NAME DELIMITER
  706. * @return string with the format NAME{{{SIZE}}}
  707. */
  708. public static function filelist_old_format($File)
  709. {
  710. $File = self::filelist_get_file($File);
  711. return $File['name'] . '{{{' . $File['size'] . '}}}';
  712. }
  713. /**
  714. * Translate a formatted file info string into a more useful array structure
  715. *
  716. * @param string $File string with the format .EXT sSIZEs NAME DELIMITER
  717. * @return file info array with the keys 'ext', 'size' and 'name'
  718. */
  719. public static function filelist_get_file($File)
  720. {
  721. // Need this hack because filelists are always display_str()ed
  722. $DelimLen = strlen(display_str(self::filelist_delim())) + 1;
  723. list($FileExt, $Size, $Name) = explode(' ', $File, 3);
  724. if ($Spaces = strspn($Name, ' ')) {
  725. $Name = str_replace(' ', '&nbsp;', substr($Name, 0, $Spaces)) . substr($Name, $Spaces);
  726. }
  727. return array(
  728. 'ext' => $FileExt,
  729. 'size' => substr($Size, 1, -1),
  730. 'name' => substr($Name, 0, -$DelimLen)
  731. );
  732. }
  733. /**
  734. * Format the information about a torrent.
  735. * @param $Data an array a subset of the following keys:
  736. * Format, Encoding, HasLog, LogScore HasCue, Media, Scene, RemasterYear
  737. * RemasterTitle, FreeTorrent, PersonalFL
  738. * @param boolean $ShowMedia if false, Media key will be omitted
  739. * @param boolean $ShowEdition if false, RemasterYear/RemasterTitle will be omitted
  740. */
  741. public static function torrent_info($Data, $ShowMedia = true, $ShowEdition = false, $HTMLy = true)
  742. {
  743. # Main torrent search results info!
  744. $Info = [];
  745. # Platform
  746. if ($ShowMedia && !empty($Data['Media'])) {
  747. $Info[] = ($HTMLy)
  748. ? '<a class="search_link" href="torrents.php?action=advanced&media='
  749. . display_str($Data['Media'])
  750. . '">'
  751. . display_str($Data['Media'])
  752. . '</a>'
  753. : display_str($Data['Media']);
  754. }
  755. # Format
  756. if (!empty($Data['Container'])) {
  757. $Info[] = ($HTMLy)
  758. ? '<a class="search_link" href="torrents.php?action=advanced&container='
  759. . display_str($Data['Container'])
  760. . '">'
  761. . display_str($Data['Container'])
  762. . '</a>'
  763. : display_str($Data['Container']);
  764. }
  765. # Archive
  766. if (!empty($Data['Archive'])) {
  767. # todo: Search on archives, lowest priority
  768. $Info[] = display_str($Data['Archive']);
  769. }
  770. # Resolution
  771. if (!empty($Data['Resolution'])) {
  772. $Info[] = ($HTMLy)
  773. ? '<a class="search_link" href="torrents.php?action=advanced&resolution='
  774. . display_str($Data['Resolution'])
  775. . '">'
  776. . display_str($Data['Resolution'])
  777. . '</a>'
  778. : display_str($Data['Resolution']);
  779. }
  780. # License
  781. if (!empty($Data['Codec'])) {
  782. $Info[] = ($HTMLy)
  783. ? '<a class="search_link" href="torrents.php?action=advanced&codec='
  784. . display_str($Data['Codec'])
  785. . '">'
  786. . display_str($Data['Codec'])
  787. . '</a>'
  788. : display_str($Data['Codec']);
  789. }
  790. # Alignned/Annotated
  791. if ($Data['Censored'] === 1) {
  792. $Info[] = ($HTMLy)
  793. ? '<a class="search_link" href="torrents.php?action=advanced&censored=1">Aligned</a>'
  794. : 'Aligned';
  795. } else {
  796. $Info[] = ($HTMLy)
  797. ? '<a class="search_link" href="torrents.php?action=advanced&censored=0">Not Aligned</a>'
  798. : 'Not Aligned';
  799. }
  800. /*
  801. if (!empty($Data['Version'])) {
  802. $Info[] = $Data['Version'];
  803. }
  804. */
  805. if ($Data['IsLeeching']) {
  806. $Info[] = $HTMLy ? Format::torrent_label('Leeching', 'important_text_semi') : 'Leeching';
  807. } elseif ($Data['IsSeeding']) {
  808. $Info[] = $HTMLy ? Format::torrent_label('Seeding', 'important_text_alt') : 'Seeding';
  809. } elseif ($Data['IsSnatched']) {
  810. $Info[] = $HTMLy ? Format::torrent_label('Snatched', 'bold') : 'Snatched';
  811. }
  812. if ($Data['FreeTorrent'] === '1') {
  813. if ($Data['FreeLeechType'] === '3') {
  814. if ($Data['ExpiryTime']) {
  815. $Info[] = ($HTMLy ? Format::torrent_label('Freeleech', 'important_text_alt') : 'Freeleech') . ($HTMLy ? " <strong>(" : " (").str_replace(['month','week','day','hour','min','s'], ['m','w','d','h','m',''], time_diff(max(strtotime($Data['ExpiryTime']), time()), 1, false)).($HTMLy ? ")</strong>" : ")");
  816. } else {
  817. $Info[] = $HTMLy ? Format::torrent_label('Freeleech', 'important_text_alt') : 'Freeleech';
  818. }
  819. } else {
  820. $Info[] = $HTMLy ? Format::torrent_label('Freeleech', 'important_text_alt') : 'Freeleech';
  821. }
  822. }
  823. if ($Data['FreeTorrent'] == '2') {
  824. $Info[] = $HTMLy ? Format::torrent_label('Neutral Leech', 'bold') : 'Neutral Leech';
  825. }
  826. if ($Data['PersonalFL']) {
  827. $Info[] = $HTMLy ? Format::torrent_label('Personal Freeleech', 'important_text_alt') : 'Personal Freeleech';
  828. }
  829. return implode(' | ', $Info);
  830. }
  831. /**
  832. * Will freeleech / neutral leech / normalise a set of torrents
  833. *
  834. * @param array $TorrentIDs An array of torrent IDs to iterate over
  835. * @param int $FreeNeutral 0 = normal, 1 = fl, 2 = nl
  836. * @param int $FreeLeechType 0 = Unknown, 1 = Staff picks, 2 = Perma-FL (Toolbox, etc.), 3 = Vanity House
  837. */
  838. public static function freeleech_torrents($TorrentIDs, $FreeNeutral = 1, $FreeLeechType = 0, $Announce = true)
  839. {
  840. if (!is_array($TorrentIDs)) {
  841. $TorrentIDs = array($TorrentIDs);
  842. }
  843. $QueryID = G::$DB->get_query_id();
  844. G::$DB->query("
  845. UPDATE torrents
  846. SET FreeTorrent = '$FreeNeutral', FreeLeechType = '$FreeLeechType'
  847. WHERE ID IN (".implode(', ', $TorrentIDs).')');
  848. G::$DB->query('
  849. SELECT ID, GroupID, info_hash
  850. FROM torrents
  851. WHERE ID IN ('.implode(', ', $TorrentIDs).')
  852. ORDER BY GroupID ASC');
  853. $Torrents = G::$DB->to_array(false, MYSQLI_NUM, false);
  854. $GroupIDs = G::$DB->collect('GroupID');
  855. G::$DB->set_query_id($QueryID);
  856. foreach ($Torrents as $Torrent) {
  857. list($TorrentID, $GroupID, $InfoHash) = $Torrent;
  858. Tracker::update_tracker('update_torrent', array('info_hash' => rawurlencode($InfoHash), 'freetorrent' => $FreeNeutral));
  859. G::$Cache->delete_value("torrent_download_$TorrentID");
  860. Misc::write_log((G::$LoggedUser['Username']??'System')." marked torrent $TorrentID freeleech type $FreeLeechType");
  861. Torrents::write_group_log($GroupID, $TorrentID, (G::$LoggedUser['ID']??0), "marked as freeleech type $FreeLeechType", 0);
  862. if ($Announce && ($FreeLeechType === 1 || $FreeLeechType === 3)) {
  863. send_irc(ANNOUNCE_CHAN, 'FREELEECH - '.site_url()."torrents.php?id=$GroupID / ".site_url()."torrents.php?action=download&id=$TorrentID");
  864. }
  865. }
  866. foreach ($GroupIDs as $GroupID) {
  867. Torrents::update_hash($GroupID);
  868. }
  869. }
  870. /**
  871. * Convenience function to allow for passing groups to Torrents::freeleech_torrents()
  872. *
  873. * @param array $GroupIDs the groups in question
  874. * @param int $FreeNeutral see Torrents::freeleech_torrents()
  875. * @param int $FreeLeechType see Torrents::freeleech_torrents()
  876. */
  877. public static function freeleech_groups($GroupIDs, $FreeNeutral = 1, $FreeLeechType = 0)
  878. {
  879. $QueryID = G::$DB->get_query_id();
  880. if (!is_array($GroupIDs)) {
  881. $GroupIDs = [$GroupIDs];
  882. }
  883. G::$DB->query('
  884. SELECT ID
  885. FROM torrents
  886. WHERE GroupID IN ('.implode(', ', $GroupIDs).')');
  887. if (G::$DB->has_results()) {
  888. $TorrentIDs = G::$DB->collect('ID');
  889. Torrents::freeleech_torrents($TorrentIDs, $FreeNeutral, $FreeLeechType);
  890. }
  891. G::$DB->set_query_id($QueryID);
  892. }
  893. /**
  894. * Check if the logged in user has an active freeleech token
  895. *
  896. * @param int $TorrentID
  897. * @return true if an active token exists
  898. */
  899. public static function has_token($TorrentID)
  900. {
  901. if (empty(G::$LoggedUser)) {
  902. return false;
  903. }
  904. static $TokenTorrents;
  905. $UserID = G::$LoggedUser['ID'];
  906. if (!isset($TokenTorrents)) {
  907. $TokenTorrents = G::$Cache->get_value("users_tokens_$UserID");
  908. if ($TokenTorrents === false) {
  909. $QueryID = G::$DB->get_query_id();
  910. G::$DB->query("
  911. SELECT TorrentID
  912. FROM users_freeleeches
  913. WHERE UserID = ?
  914. AND Expired = 0", $UserID);
  915. $TokenTorrents = array_fill_keys(G::$DB->collect('TorrentID', false), true);
  916. G::$DB->set_query_id($QueryID);
  917. G::$Cache->cache_value("users_tokens_$UserID", $TokenTorrents);
  918. }
  919. }
  920. return isset($TokenTorrents[$TorrentID]);
  921. }
  922. /**
  923. * Check if the logged in user can use a freeleech token on this torrent
  924. *
  925. * @param int $Torrent
  926. * @return boolen True if user is allowed to use a token
  927. */
  928. public static function can_use_token($Torrent)
  929. {
  930. if (empty(G::$LoggedUser)) {
  931. return false;
  932. }
  933. return (G::$LoggedUser['FLTokens'] > 0
  934. && $Torrent['Size'] <= 10737418240
  935. && !$Torrent['PersonalFL']
  936. && empty($Torrent['FreeTorrent'])
  937. && G::$LoggedUser['CanLeech'] == '1');
  938. }
  939. /**
  940. * Build snatchlists and check if a torrent has been snatched
  941. * if a user has the 'ShowSnatched' option enabled
  942. * @param int $TorrentID
  943. * @return bool
  944. */
  945. public static function has_snatched($TorrentID)
  946. {
  947. if (empty(G::$LoggedUser) || !isset(G::$LoggedUser['ShowSnatched']) || !G::$LoggedUser['ShowSnatched']) {
  948. return false;
  949. }
  950. $UserID = G::$LoggedUser['ID'];
  951. $Buckets = 64;
  952. $LastBucket = $Buckets - 1;
  953. $BucketID = $TorrentID & $LastBucket;
  954. static $SnatchedTorrents = [], $UpdateTime = [];
  955. if (empty($SnatchedTorrents)) {
  956. $SnatchedTorrents = array_fill(0, $Buckets, false);
  957. $UpdateTime = G::$Cache->get_value("users_snatched_{$UserID}_time");
  958. if ($UpdateTime === false) {
  959. $UpdateTime = array(
  960. 'last' => 0,
  961. 'next' => 0);
  962. }
  963. } elseif (isset($SnatchedTorrents[$BucketID][$TorrentID])) {
  964. return true;
  965. }
  966. // Torrent was not found in the previously inspected snatch lists
  967. $CurSnatchedTorrents =& $SnatchedTorrents[$BucketID];
  968. if ($CurSnatchedTorrents === false) {
  969. $CurTime = time();
  970. // This bucket hasn't been checked before
  971. $CurSnatchedTorrents = G::$Cache->get_value("users_snatched_{$UserID}_$BucketID", true);
  972. if ($CurSnatchedTorrents === false || $CurTime > $UpdateTime['next']) {
  973. $Updated = [];
  974. $QueryID = G::$DB->get_query_id();
  975. if ($CurSnatchedTorrents === false || $UpdateTime['last'] == 0) {
  976. for ($i = 0; $i < $Buckets; $i++) {
  977. $SnatchedTorrents[$i] = [];
  978. }
  979. // Not found in cache. Since we don't have a suitable index, it's faster to update everything
  980. G::$DB->query("
  981. SELECT fid
  982. FROM xbt_snatched
  983. WHERE uid = ?", $UserID);
  984. while (list($ID) = G::$DB->next_record(MYSQLI_NUM, false)) {
  985. $SnatchedTorrents[$ID & $LastBucket][(int)$ID] = true;
  986. }
  987. $Updated = array_fill(0, $Buckets, true);
  988. } elseif (isset($CurSnatchedTorrents[$TorrentID])) {
  989. // Old cache, but torrent is snatched, so no need to update
  990. return true;
  991. } else {
  992. // Old cache, check if torrent has been snatched recently
  993. G::$DB->query("
  994. SELECT fid
  995. FROM xbt_snatched
  996. WHERE uid = ?
  997. AND tstamp >= ?", $UserID, $UpdateTime['last']);
  998. while (list($ID) = G::$DB->next_record(MYSQLI_NUM, false)) {
  999. $CurBucketID = $ID & $LastBucket;
  1000. if ($SnatchedTorrents[$CurBucketID] === false) {
  1001. $SnatchedTorrents[$CurBucketID] = G::$Cache->get_value("users_snatched_{$UserID}_$CurBucketID", true);
  1002. if ($SnatchedTorrents[$CurBucketID] === false) {
  1003. $SnatchedTorrents[$CurBucketID] = [];
  1004. }
  1005. }
  1006. $SnatchedTorrents[$CurBucketID][(int)$ID] = true;
  1007. $Updated[$CurBucketID] = true;
  1008. }
  1009. }
  1010. G::$DB->set_query_id($QueryID);
  1011. for ($i = 0; $i < $Buckets; $i++) {
  1012. if (isset($Updated[$i])) {
  1013. G::$Cache->cache_value("users_snatched_{$UserID}_$i", $SnatchedTorrents[$i], 0);
  1014. }
  1015. }
  1016. $UpdateTime['last'] = $CurTime;
  1017. $UpdateTime['next'] = $CurTime + self::SNATCHED_UPDATE_INTERVAL;
  1018. G::$Cache->cache_value("users_snatched_{$UserID}_time", $UpdateTime, 0);
  1019. }
  1020. }
  1021. return isset($CurSnatchedTorrents[$TorrentID]);
  1022. }
  1023. public static function is_seeding($TorrentID)
  1024. {
  1025. if (empty(G::$LoggedUser) || !isset(G::$LoggedUser['ShowSnatched']) || !G::$LoggedUser['ShowSnatched']) {
  1026. return false;
  1027. }
  1028. $UserID = G::$LoggedUser['ID'];
  1029. $Buckets = 64;
  1030. $LastBucket = $Buckets - 1;
  1031. $BucketID = $TorrentID & $LastBucket;
  1032. static $SeedingTorrents = [], $UpdateTime = [];
  1033. if (empty($SeedingTorrents)) {
  1034. $SeedingTorrents = array_fill(0, $Buckets, false);
  1035. $UpdateTime = G::$Cache->get_value("users_seeding_{$UserID}_time");
  1036. if ($UpdateTime === false) {
  1037. $UpdateTime = array(
  1038. 'last' => 0,
  1039. 'next' => 0);
  1040. }
  1041. } elseif (isset($SeedingTorrents[$BucketID][$TorrentID])) {
  1042. return true;
  1043. }
  1044. // Torrent was not found in the previously inspected seeding lists
  1045. $CurSeedingTorrents =& $SeedingTorrents[$BucketID];
  1046. if ($CurSeedingTorrents === false) {
  1047. $CurTime = time();
  1048. // This bucket hasn't been checked before
  1049. $CurSeedingTorrents = G::$Cache->get_value("users_seeding_{$UserID}_$BucketID", true);
  1050. if ($CurSeedingTorrents === false || $CurTime > $UpdateTime['next']) {
  1051. $Updated = [];
  1052. $QueryID = G::$DB->get_query_id();
  1053. if ($CurSeedingTorrents === false || $UpdateTime['last'] == 0) {
  1054. for ($i = 0; $i < $Buckets; $i++) {
  1055. $SeedingTorrents[$i] = [];
  1056. }
  1057. // Not found in cache. Since we don't have a suitable index, it's faster to update everything
  1058. G::$DB->query("
  1059. SELECT fid
  1060. FROM xbt_files_users
  1061. WHERE uid = ?
  1062. AND active = 1
  1063. AND Remaining = 0", $UserID);
  1064. while (list($ID) = G::$DB->next_record(MYSQLI_NUM, false)) {
  1065. $SeedingTorrents[$ID & $LastBucket][(int)$ID] = true;
  1066. }
  1067. $Updated = array_fill(0, $Buckets, true);
  1068. } elseif (isset($CurSeedingTorrents[$TorrentID])) {
  1069. // Old cache, but torrent is seeding, so no need to update
  1070. return true;
  1071. } else {
  1072. // Old cache, check if torrent has been seeding recently
  1073. G::$DB->query("
  1074. SELECT fid
  1075. FROM xbt_files_users
  1076. WHERE uid = ?
  1077. AND active = 1
  1078. AND Remaining = 0
  1079. AND mtime >= ?", $UserID, $UpdateTime['last']);
  1080. while (list($ID) = G::$DB->next_record(MYSQLI_NUM, false)) {
  1081. $CurBucketID = $ID & $LastBucket;
  1082. if ($SeedingTorrents[$CurBucketID] === false) {
  1083. $SeedingTorrents[$CurBucketID] = G::$Cache->get_value("users_seeding_{$UserID}_$CurBucketID", true);
  1084. if ($SeedingTorrents[$CurBucketID] === false) {
  1085. $SeedingTorrents[$CurBucketID] = [];
  1086. }
  1087. }
  1088. $SeedingTorrents[$CurBucketID][(int)$ID] = true;
  1089. $Updated[$CurBucketID] = true;
  1090. }
  1091. }
  1092. G::$DB->set_query_id($QueryID);
  1093. for ($i = 0; $i < $Buckets; $i++) {
  1094. if (isset($Updated[$i])) {
  1095. G::$Cache->cache_value("users_seeding_{$UserID}_$i", $SeedingTorrents[$i], 3600);
  1096. }
  1097. }
  1098. $UpdateTime['last'] = $CurTime;
  1099. $UpdateTime['next'] = $CurTime + self::SNATCHED_UPDATE_INTERVAL;
  1100. G::$Cache->cache_value("users_seeding_{$UserID}_time", $UpdateTime, 3600);
  1101. }
  1102. }
  1103. return isset($CurSeedingTorrents[$TorrentID]);
  1104. }
  1105. public static function is_leeching($TorrentID)
  1106. {
  1107. if (empty(G::$LoggedUser) || !isset(G::$LoggedUser['ShowSnatched']) || !G::$LoggedUser['ShowSnatched']) {
  1108. return false;
  1109. }
  1110. $UserID = G::$LoggedUser['ID'];
  1111. $Buckets = 64;
  1112. $LastBucket = $Buckets - 1;
  1113. $BucketID = $TorrentID & $LastBucket;
  1114. static $LeechingTorrents = [], $UpdateTime = [];
  1115. if (empty($LeechingTorrents)) {
  1116. $LeechingTorrents = array_fill(0, $Buckets, false);
  1117. $UpdateTime = G::$Cache->get_value("users_leeching_{$UserID}_time");
  1118. if ($UpdateTime === false) {
  1119. $UpdateTime = array(
  1120. 'last' => 0,
  1121. 'next' => 0);
  1122. }
  1123. } elseif (isset($LeechingTorrents[$BucketID][$TorrentID])) {
  1124. return true;
  1125. }
  1126. // Torrent was not found in the previously inspected snatch lists
  1127. $CurLeechingTorrents =& $LeechingTorrents[$BucketID];
  1128. if ($CurLeechingTorrents === false) {
  1129. $CurTime = time();
  1130. // This bucket hasn't been checked before
  1131. $CurLeechingTorrents = G::$Cache->get_value("users_leeching_{$UserID}_$BucketID", true);
  1132. if ($CurLeechingTorrents === false || $CurTime > $UpdateTime['next']) {
  1133. $Updated = [];
  1134. $QueryID = G::$DB->get_query_id();
  1135. if ($CurLeechingTorrents === false || $UpdateTime['last'] == 0) {
  1136. for ($i = 0; $i < $Buckets; $i++) {
  1137. $LeechingTorrents[$i] = [];
  1138. }
  1139. // Not found in cache. Since we don't have a suitable index, it's faster to update everything
  1140. G::$DB->query("
  1141. SELECT fid
  1142. FROM xbt_files_users
  1143. WHERE uid = ?
  1144. AND active = 1
  1145. AND Remaining > 0", $UserID);
  1146. while (list($ID) = G::$DB->next_record(MYSQLI_NUM, false)) {
  1147. $LeechingTorrents[$ID & $LastBucket][(int)$ID] = true;
  1148. }
  1149. $Updated = array_fill(0, $Buckets, true);
  1150. } elseif (isset($CurLeechingTorrents[$TorrentID])) {
  1151. // Old cache, but torrent is leeching, so no need to update
  1152. return true;
  1153. } else {
  1154. // Old cache, check if torrent has been leeching recently
  1155. G::$DB->query("
  1156. SELECT fid
  1157. FROM xbt_files_users
  1158. WHERE uid = ?
  1159. AND active = 1
  1160. AND Remaining > 0
  1161. AND mtime >= ?", $UserID, $UpdateTime['last']);
  1162. while (list($ID) = G::$DB->next_record(MYSQLI_NUM, false)) {
  1163. $CurBucketID = $ID & $LastBucket;
  1164. if ($LeechingTorrents[$CurBucketID] === false) {
  1165. $LeechingTorrents[$CurBucketID] = G::$Cache->get_value("users_leeching_{$UserID}_$CurBucketID", true);
  1166. if ($LeechingTorrents[$CurBucketID] === false) {
  1167. $LeechingTorrents[$CurBucketID] = [];
  1168. }
  1169. }
  1170. $LeechingTorrents[$CurBucketID][(int)$ID] = true;
  1171. $Updated[$CurBucketID] = true;
  1172. }
  1173. }
  1174. G::$DB->set_query_id($QueryID);
  1175. for ($i = 0; $i < $Buckets; $i++) {
  1176. if (isset($Updated[$i])) {
  1177. G::$Cache->cache_value("users_leeching_{$UserID}_$i", $LeechingTorrents[$i], 3600);
  1178. }
  1179. }
  1180. $UpdateTime['last'] = $CurTime;
  1181. $UpdateTime['next'] = $CurTime + self::SNATCHED_UPDATE_INTERVAL;
  1182. G::$Cache->cache_value("users_leeching_{$UserID}_time", $UpdateTime, 3600);
  1183. }
  1184. }
  1185. return isset($CurLeechingTorrents[$TorrentID]);
  1186. }
  1187. /*
  1188. public static function is_seeding_or_leeching($TorrentID) {
  1189. if (empty(G::$LoggedUser))
  1190. return false;
  1191. $UserID = G::$LoggedUser['ID'];
  1192. $Result = array("IsSeeding" => false, "IsLeeching" => false);
  1193. $QueryID = G::$DB->get_query_id();
  1194. G::$DB->query("
  1195. SELECT Remaining
  1196. FROM xbt_files_users
  1197. WHERE fid = $TorrentID
  1198. AND uid = $UserID
  1199. AND active = 1");
  1200. if (G::$DB->has_results()) {
  1201. while (($Row = G::$DB->next_record(MYSQLI_ASSOC, true)) && !($Result['IsSeeding'] && $Result['IsLeeching'])) {
  1202. if ($Row['Remaining'] == 0)
  1203. $Result['IsSeeding'] = true;
  1204. if ($Row['Remaining'] > 0)
  1205. $Result['IsLeeching'] = true;
  1206. }
  1207. }
  1208. G::$DB->set_query_id($QueryID);
  1209. return $Result;
  1210. }
  1211. */
  1212. /**
  1213. * Change the schedule for when the next update to a user's cached snatch list should be performed.
  1214. * By default, the change will only be made if the new update would happen sooner than the current
  1215. * @param int $Time Seconds until the next update
  1216. * @param bool $Force Whether to accept changes that would push back the update
  1217. */
  1218. public static function set_snatch_update_time($UserID, $Time, $Force = false)
  1219. {
  1220. if (!$UpdateTime = G::$Cache->get_value("users_snatched_{$UserID}_time")) {
  1221. return;
  1222. }
  1223. $NextTime = time() + $Time;
  1224. if ($Force || $NextTime < $UpdateTime['next']) {
  1225. // Skip if the change would delay the next update
  1226. $UpdateTime['next'] = $NextTime;
  1227. G::$Cache->cache_value("users_snatched_{$UserID}_time", $UpdateTime, 0);
  1228. }
  1229. }
  1230. // Some constants for self::display_string's $Mode parameter
  1231. const DISPLAYSTRING_HTML = 1; // Whether or not to use HTML for the output (e.g. VH tooltip)
  1232. const DISPLAYSTRING_ARTISTS = 2; // Whether or not to display artists
  1233. const DISPLAYSTRING_YEAR = 4; // Whether or not to display the group's year
  1234. const DISPLAYSTRING_VH = 8; // Whether or not to display the VH flag
  1235. const DISPLAYSTRING_RELEASETYPE = 16; // Whether or not to display the release type
  1236. const DISPLAYSTRING_LINKED = 33; // Whether or not to link artists and the group
  1237. // The constant for linking is 32, but because linking only works with HTML, this constant is defined as 32|1 = 33, i.e. LINKED also includes HTML
  1238. // Keep this in mind when defining presets below!
  1239. // Presets to facilitate the use of $Mode
  1240. const DISPLAYSTRING_DEFAULT = 63; // HTML|ARTISTS|YEAR|VH|RELEASETYPE|LINKED = 63
  1241. const DISPLAYSTRING_SHORT = 6; // Very simple format, only artists and year, no linking (e.g. for forum thread titles)
  1242. /**
  1243. * Return the display string for a given torrent group $GroupID.
  1244. * @param int $GroupID
  1245. * @return string
  1246. */
  1247. public static function display_string($GroupID, $Mode = self::DISPLAYSTRING_DEFAULT)
  1248. {
  1249. #global $ReleaseTypes; // I hate this
  1250. $GroupInfo = self::get_groups(array($GroupID), true, true, false)[$GroupID];
  1251. $ExtendedArtists = $GroupInfo['ExtendedArtists'];
  1252. if ($Mode & self::DISPLAYSTRING_ARTISTS) {
  1253. if (!empty($ExtendedArtists[1])
  1254. || !empty($ExtendedArtists[4])
  1255. || !empty($ExtendedArtists[5])
  1256. || !empty($ExtendedArtists[6])
  1257. ) {
  1258. unset($ExtendedArtists[2], $ExtendedArtists[3]);
  1259. $DisplayName = Artists::display_artists($ExtendedArtists, ($Mode & self::DISPLAYSTRING_LINKED));
  1260. } else {
  1261. $DisplayName = '';
  1262. }
  1263. }
  1264. if ($Mode & self::DISPLAYSTRING_LINKED) {
  1265. $DisplayName .= "<a href=\"torrents.php?id=$GroupID\" class=\"tooltip\" title=\"View torrent group\" dir=\"ltr\">$GroupInfo[Name]</a>";
  1266. } else {
  1267. $DisplayName .= $GroupInfo['Name'];
  1268. }
  1269. if (($Mode & self::DISPLAYSTRING_YEAR) && $GroupInfo['Year'] > 0) {
  1270. $DisplayName .= " [$GroupInfo[Year]]";
  1271. }
  1272. if (($Mode & self::DISPLAYSTRING_RELEASETYPE) && $GroupInfo['ReleaseType'] > 0) {
  1273. $DisplayName .= ' ['.$ReleaseTypes[$GroupInfo['ReleaseType']].']';
  1274. }
  1275. return $DisplayName;
  1276. }
  1277. public static function edition_string(array $Torrent, array $Group)
  1278. {
  1279. /*
  1280. $AddExtra = '&thinsp;|&thinsp;'; # breaking
  1281. $EditionName = 'Original Release';
  1282. $EditionName .= $AddExtra . display_str($Torrent['Media']);
  1283. return $EditionName;
  1284. */
  1285. }
  1286. // Used to get reports info on a unison cache in both browsing pages and torrent pages.
  1287. public static function get_reports($TorrentID)
  1288. {
  1289. $Reports = G::$Cache->get_value("reports_torrent_$TorrentID");
  1290. if ($Reports === false) {
  1291. $QueryID = G::$DB->get_query_id();
  1292. G::$DB->query("
  1293. SELECT
  1294. ID,
  1295. ReporterID,
  1296. Type,
  1297. UserComment,
  1298. ReportedTime
  1299. FROM reportsv2
  1300. WHERE TorrentID = ?
  1301. AND Status != 'Resolved'", $TorrentID);
  1302. $Reports = G::$DB->to_array(false, MYSQLI_ASSOC, false);
  1303. G::$DB->set_query_id($QueryID);
  1304. G::$Cache->cache_value("reports_torrent_$TorrentID", $Reports, 0);
  1305. }
  1306. if (!check_perms('admin_reports')) {
  1307. $Return = [];
  1308. foreach ($Reports as $Report) {
  1309. if ($Report['Type'] !== 'edited') {
  1310. $Return[] = $Report;
  1311. }
  1312. }
  1313. return $Return;
  1314. }
  1315. return $Reports;
  1316. }
  1317. }