( * ID * Name * Year * RecordLabel * CatalogueNumber * TagList * ReleaseType * VanityHouse * WikiImage * CategoryID * Torrents => { * ID => { * GroupID, Media, Format, Encoding, RemasterYear, Remastered, * RemasterTitle, RemasterRecordLabel, RemasterCatalogueNumber, Scene, * HasLog, HasCue, LogScore, FileCount, FreeTorrent, Size, Leechers, * Seeders, Snatched, Time, HasFile, PersonalFL, IsSnatched * } * } * Artists => { * { * id, name, aliasid // Only main artists * } * } * ExtendedArtists => { * [1-6] => { // See documentation on Artists::get_artists * id, name, aliasid * } * } * Flags => { * IsSnatched * } */ public static function get_groups($GroupIDs, $Return = true, $GetArtists = true, $Torrents = true) { $Found = $NotFound = array_fill_keys($GroupIDs, false); $Key = $Torrents ? 'torrent_group_' : 'torrent_group_light_'; foreach ($GroupIDs as $i => $GroupID) { if (!is_number($GroupID)) { unset($GroupIDs[$i], $Found[$GroupID], $NotFound[$GroupID]); continue; } $Data = G::$Cache->get_value($Key . $GroupID, true); if (!empty($Data) && is_array($Data) && $Data['ver'] === Cache::GROUP_VERSION) { unset($NotFound[$GroupID]); $Found[$GroupID] = $Data['d']; } } // Make sure there's something in $GroupIDs, otherwise the SQL will break if (count($GroupIDs) === 0) { return []; } /** * Changing any of these attributes returned will cause very large, very dramatic site-wide chaos. * Do not change what is returned or the order thereof without updating: * torrents, artists, collages, bookmarks, better, the front page, * and anywhere else the get_groups function is used. * Update self::array_group(), too. */ if (count($NotFound) > 0) { $IDs = implode(',', array_keys($NotFound)); $NotFound = []; $QueryID = G::$DB->get_query_id(); G::$DB->prepare_query(" SELECT `id`, `title`, `subject`, `object`, `year`, `identifier`, `workgroup`, `location`, `tag_list`, `picture`, `category_id` FROM `torrents_group` WHERE `id` IN($IDs) "); G::$DB->exec_prepared_query(); while ($Group = G::$DB->next_record(MYSQLI_ASSOC, true)) { $NotFound[$Group['id']] = $Group; $NotFound[$Group['id']]['Torrents'] = []; $NotFound[$Group['id']]['Artists'] = []; } G::$DB->set_query_id($QueryID); if ($Torrents) { $QueryID = G::$DB->get_query_id(); G::$DB->query(" SELECT `ID`, `GroupID`, `Media`, `Container`, `Codec`, `Resolution`, `Version`, `Censored`, `Archive`, `FileCount`, `FreeTorrent`, `Size`, `Leechers`, `Seeders`, `Snatched`, `Time`, f.`ExpiryTime`, `ID` AS `HasFile`, `FreeLeechType`, HEX(`info_hash`) AS `info_hash` FROM `torrents` LEFT JOIN `shop_freeleeches` AS f ON f.`TorrentID` = `ID` WHERE `GroupID` IN($IDs) ORDER BY `GroupID`, `Media`, `Container`, `Codec`, `ID` "); while ($Torrent = G::$DB->next_record(MYSQLI_ASSOC, true)) { $NotFound[$Torrent['GroupID']]['Torrents'][$Torrent['ID']] = $Torrent; } G::$DB->set_query_id($QueryID); } foreach ($NotFound as $GroupID => $GroupInfo) { G::$Cache->cache_value($Key . $GroupID, array('ver' => Cache::GROUP_VERSION, 'd' => $GroupInfo), 0); } $Found = $NotFound + $Found; } // Filter out orphans (elements that are === false) $Found = array_filter($Found); if ($GetArtists) { $Artists = Artists::get_artists($GroupIDs); } else { $Artists = []; } if ($Return) { // If we're interested in the data, and not just caching it foreach ($Artists as $GroupID => $Data) { if (!isset($Found[$GroupID])) { continue; } $Found[$GroupID]['Artists'] = $Data; } // Fetch all user specific torrent properties if ($Torrents) { foreach ($Found as &$Group) { $Group['Flags'] = array('IsSnatched' => false, 'IsSeeding' => false, 'IsLeeching' => false); if (!empty($Group['Torrents'])) { foreach ($Group['Torrents'] as &$Torrent) { self::torrent_properties($Torrent, $Group['Flags']); } } } } return $Found; } } /** * Returns a reconfigured array from a Torrent Group * * Use this with extract() instead of the volatile list($GroupID, ...) * Then use the variables $GroupID, $GroupName, etc * * @example extract(Torrents::array_group($SomeGroup)); * @param array $Group torrent group * @return array Re-key'd array */ public static function array_group(array &$Group) { return array( 'id' => $Group['id'], 'title' => $Group['title'], 'subject' => $Group['subject'], 'object' => $Group['object'], 'published' => $Group['published'], 'category_id' => $Group['category_id'], 'identifier' => $Group['identifier'], 'workgroup' => $Group['workgroup'], 'location' => $Group['location'], 'GroupFlags' => ($Group['Flags'] ?? ''), 'tag_list' => $Group['tag_list'], 'picture' => $Group['picture'], 'Torrents' => $Group['Torrents'], 'Artists' => $Group['Artists'] ); } /** * Supplements a torrent array with information that only concerns certain users and therefore cannot be cached * * @param array $Torrent torrent array preferably in the form used by Torrents::get_groups() or get_group_info() * @param int $TorrentID */ public static function torrent_properties(&$Torrent, &$Flags) { # FL Token $Torrent['PersonalFL'] = empty($Torrent['FreeTorrent']) && self::has_token($Torrent['ID']); # Snatched if ($Torrent['IsSnatched'] = self::has_snatched($Torrent['ID'])) { $Flags['IsSnatched'] = true; } else { $Flags['IsSnatched'] = false; } # Seeding if ($Torrent['IsSeeding'] = self::is_seeding($Torrent['ID'])) { $Flags['IsSeeding'] = true; } else { $Flags['IsSeeding'] = false; } # Leeching if ($Torrent['IsLeeching'] = self::is_leeching($Torrent['ID'])) { $Flags['IsLeeching'] = true; } else { $Flags['IsLeeching'] = false; } } /* * Write to the group log. * * @param int $GroupID * @param int $TorrentID * @param int $UserID * @param string $Message * @param boolean $Hidden Currently does fuck all. * * todo: Fix that */ public static function write_group_log($GroupID, $TorrentID, $UserID, $Message, $Hidden) { global $Time; $QueryID = G::$DB->get_query_id(); G::$DB->query(" INSERT INTO `group_log`( `GroupID`, `TorrentID`, `UserID`, `Info`, `Time`, `Hidden` ) VALUES( '$GroupID', '$TorrentID', '$UserID', '$Message', NOW(), '$Hidden' ) "); G::$DB->set_query_id($QueryID); } /** * Delete a torrent. * * @param int $ID The ID of the torrent to delete. * @param int $GroupID Set it if you have it handy, to save a query. Otherwise, it will be found. * @param string $OcelotReason The deletion reason for ocelot to report to users. */ public static function delete_torrent($ID, $GroupID = 0, $OcelotReason = -1) { $QueryID = G::$DB->get_query_id(); if (!$GroupID) { G::$DB->query(" SELECT GroupID, UserID FROM torrents WHERE ID = '$ID'"); list($GroupID, $UploaderID) = G::$DB->next_record(); } if (empty($UserID)) { G::$DB->query(" SELECT UserID FROM torrents WHERE ID = '$ID'"); list($UserID) = G::$DB->next_record(); } $RecentUploads = G::$Cache->get_value("recent_uploads_$UserID"); if (is_array($RecentUploads)) { foreach ($RecentUploads as $Key => $Recent) { if ($Recent['ID'] == $GroupID) { G::$Cache->delete_value("recent_uploads_$UserID"); } } } G::$DB->query(" SELECT info_hash FROM torrents WHERE ID = $ID"); list($InfoHash) = G::$DB->next_record(MYSQLI_BOTH, false); G::$DB->query(" DELETE FROM torrents WHERE ID = $ID"); Tracker::update_tracker('delete_torrent', array('info_hash' => rawurlencode($InfoHash), 'id' => $ID, 'reason' => $OcelotReason)); G::$Cache->decrement('stats_torrent_count'); G::$DB->query(" SELECT COUNT(ID) FROM torrents WHERE GroupID = '$GroupID'"); list($Count) = G::$DB->next_record(); if ($Count == 0) { Torrents::delete_group($GroupID); } else { Torrents::update_hash($GroupID); } // Torrent notifications G::$DB->query(" SELECT UserID FROM users_notify_torrents WHERE TorrentID = '$ID'"); while (list($UserID) = G::$DB->next_record()) { G::$Cache->delete_value("notifications_new_$UserID"); } G::$DB->query(" DELETE FROM users_notify_torrents WHERE TorrentID = '$ID'"); G::$DB->query(" UPDATE reportsv2 SET Status = 'Resolved', LastChangeTime = NOW(), ModComment = 'Report already dealt with (torrent deleted)' WHERE TorrentID = ? AND Status != 'Resolved'", $ID); $Reports = G::$DB->affected_rows(); if ($Reports) { G::$Cache->decrement('num_torrent_reportsv2', $Reports); } unlink(TORRENT_STORE.$ID.'.torrent'); G::$DB->query(" DELETE FROM torrents_bad_tags WHERE TorrentID = ?", $ID); G::$DB->query(" DELETE FROM torrents_bad_folders WHERE TorrentID = ?", $ID); G::$DB->query(" DELETE FROM torrents_bad_files WHERE TorrentID = ?", $ID); G::$DB->query(" DELETE FROM shop_freeleeches WHERE TorrentID = ?", $ID); $FLs = G::$DB->affected_rows(); if ($FLs) { G::$Cache->delete_value('shop_freeleech_list'); } // Tells Sphinx that the group is removed G::$DB->query(" REPLACE INTO sphinx_delta (ID, Time) VALUES (?, UNIX_TIMESTAMP())", $ID); G::$Cache->delete_value("torrent_download_$ID"); G::$Cache->delete_value("torrent_group_$GroupID"); G::$Cache->delete_value("torrents_details_$GroupID"); G::$DB->set_query_id($QueryID); } /** * Delete a group, called after all of its torrents have been deleted. * IMPORTANT: Never call this unless you're certain the group is no longer used by any torrents * * @param int $GroupID */ public static function delete_group($GroupID) { $QueryID = G::$DB->get_query_id(); Misc::write_log("Group $GroupID automatically deleted (No torrents have this group)."); G::$DB->prepare_query(" SELECT `category_id` FROM `torrents_group` WHERE `id` = '$GroupID' "); G::$DB->exec_prepared_query(); list($Category) = G::$DB->next_record(); # todo: Check strict equality here if ($Category === 1) { G::$Cache->decrement('stats_album_count'); } G::$Cache->decrement('stats_group_count'); // Collages G::$DB->query(" SELECT CollageID FROM collages_torrents WHERE GroupID = ?", $GroupID); if (G::$DB->has_results()) { $CollageIDs = G::$DB->collect('CollageID'); G::$DB->query(" UPDATE collages SET NumTorrents = NumTorrents - 1 WHERE ID IN (".implode(', ', $CollageIDs).')'); G::$DB->query(" DELETE FROM collages_torrents WHERE GroupID = ?", $GroupID); foreach ($CollageIDs as $CollageID) { G::$Cache->delete_value("collage_$CollageID"); } G::$Cache->delete_value("torrent_collages_$GroupID"); } // Artists // Collect the artist IDs and then wipe the torrents_artist entry G::$DB->query(" SELECT ArtistID FROM torrents_artists WHERE GroupID = ?", $GroupID); $Artists = G::$DB->collect('ArtistID'); G::$DB->query(" DELETE FROM torrents_artists WHERE GroupID = ?", $GroupID); foreach ($Artists as $ArtistID) { if (empty($ArtistID)) { continue; } // Get a count of how many groups or requests use the artist ID G::$DB->query(" SELECT COUNT(ag.ArtistID) FROM artists_group AS ag LEFT JOIN requests_artists AS ra ON ag.ArtistID = ra.ArtistID WHERE ra.ArtistID IS NOT NULL AND ag.ArtistID = ?", $ArtistID); list($ReqCount) = G::$DB->next_record(); G::$DB->query(" SELECT COUNT(ag.ArtistID) FROM artists_group AS ag LEFT JOIN torrents_artists AS ta ON ag.ArtistID = ta.ArtistID WHERE ta.ArtistID IS NOT NULL AND ag.ArtistID = ?", $ArtistID); list($GroupCount) = G::$DB->next_record(); if (($ReqCount + $GroupCount) == 0) { //The only group to use this artist Artists::delete_artist($ArtistID); } else { //Not the only group, still need to clear cache G::$Cache->delete_value("artist_groups_$ArtistID"); } } // Requests G::$DB->query(" SELECT ID FROM requests WHERE GroupID = ?", $GroupID); $Requests = G::$DB->collect('ID'); G::$DB->query(" UPDATE requests SET GroupID = NULL WHERE GroupID = ?", $GroupID); foreach ($Requests as $RequestID) { G::$Cache->delete_value("request_$RequestID"); } // Comments Comments::delete_page('torrents', $GroupID); G::$DB->prepare_query(" DELETE FROM `torrents_group` WHERE `id` = '$GroupID' "); G::$DB->exec_prepared_query(); G::$DB->prepare_query(" DELETE FROM `torrents_tags` WHERE `GroupID` = '$GroupID' "); G::$DB->exec_prepared_query(); G::$DB->prepare_query(" DELETE FROM `bookmarks_torrents` WHERE `GroupID` = '$GroupID' "); G::$DB->exec_prepared_query(); G::$DB->prepare_query(" DELETE FROM `wiki_torrents` WHERE `PageID` = '$GroupID' "); G::$DB->exec_prepared_query(); G::$Cache->delete_value("torrents_details_$GroupID"); G::$Cache->delete_value("torrent_group_$GroupID"); G::$Cache->delete_value("groups_artists_$GroupID"); G::$DB->set_query_id($QueryID); } /** * Update the cache and sphinx delta index to keep everything up-to-date. * * @param int $GroupID */ public static function update_hash(int $GroupID) { $QueryID = G::$DB->get_query_id(); G::$DB->prepare_query(" UPDATE `torrents_group` SET `tag_list` =( SELECT REPLACE ( GROUP_CONCAT(tags.Name SEPARATOR ' '), '.', '_' ) FROM `torrents_tags` AS t INNER JOIN `tags` ON tags.`ID` = t.`TagID` WHERE t.`GroupID` = '$GroupID' GROUP BY t.`GroupID` ) WHERE `ID` = '$GroupID' "); G::$DB->exec_prepared_query(); // Fetch album artists G::$DB->prepare_query(" SELECT GROUP_CONCAT(ag.`Name` separator ' ') FROM `torrents_artists` AS `ta` JOIN `artists_group` AS ag ON ag.`ArtistID` = ta.`ArtistID` WHERE ta.`GroupID` = '$GroupID' GROUP BY ta.`GroupID` "); G::$DB->exec_prepared_query(); if (G::$DB->has_results()) { list($ArtistName) = G::$DB->next_record(MYSQLI_NUM, false); } else { $ArtistName = ''; } G::$DB->prepare_query(" REPLACE INTO sphinx_delta( `ID`, `GroupID`, `GroupName`, `GroupTitle2`, `GroupNameJP`, `TagList`, `Year`, `CatalogueNumber`, `CategoryID`, `Time`, `Size`, `Snatched`, `Seeders`, `Leechers`, `Censored`, `Studio`, `Series`, `FreeTorrent`, `Media`, `Container`, `Codec`, `Resolution`, `Version`, `Description`, `FileList`, `ArtistName` ) SELECT t.`ID`, g.`id`, g.`title`, g.`subject`, g.`object`, g.`tag_list`, g.`year`, g.`identifier`, g.`category_id`, UNIX_TIMESTAMP(t.`Time`), t.`Size`, t.`Snatched`, t.`Seeders`, t.`Leechers`, t.`Censored`, g.`workgroup`, g.`location`, CAST(`FreeTorrent` AS CHAR), t.`Media`, t.`Container`, t.`Codec`, t.`Resolution`, t.`Version`, t.`Description`, REPLACE ( REPLACE (`FileList`, '_', ' '), '/', ' ' ) AS FileList, '$ArtistName' FROM `torrents` AS t JOIN `torrents_group` AS g ON g.`id` = t.`GroupID` WHERE g.`id` = '$GroupID' "); G::$DB->exec_prepared_query(); G::$Cache->delete_value("torrents_details_$GroupID"); G::$Cache->delete_value("torrent_group_$GroupID"); G::$Cache->delete_value("torrent_group_light_$GroupID"); $ArtistInfo = Artists::get_artist($GroupID); G::$Cache->delete_value("groups_artists_$GroupID"); G::$DB->set_query_id($QueryID); } /** * Regenerate a torrent's file list from its meta data, * update the database record and clear relevant cache keys * * @param int $TorrentID */ public static function regenerate_filelist($TorrentID) { $QueryID = G::$DB->get_query_id(); G::$DB->query(" SELECT GroupID FROM torrents WHERE ID = ?", $TorrentID); if (G::$DB->has_results()) { list($GroupID) = G::$DB->next_record(MYSQLI_NUM, false); $Contents = file_get_contents(TORRENT_STORE.$TorrentID.'.torrent'); if (Misc::is_new_torrent($Contents)) { $Tor = new BencodeTorrent($Contents); $FilePath = (isset($Tor->Dec['info']['files']) ? Format::make_utf8($Tor->get_name()) : ''); } else { $Tor = new TORRENT(unserialize(base64_decode($Contents)), true); $FilePath = (isset($Tor->Val['info']->Val['files']) ? Format::make_utf8($Tor->get_name()) : ''); } list($TotalSize, $FileList) = $Tor->file_list(); foreach ($FileList as $File) { $TmpFileList[] = self::filelist_format_file($File); } $FileString = implode("\n", $TmpFileList); G::$DB->query( " UPDATE torrents SET Size = ?, FilePath = ?, FileList = ? WHERE ID = ?", $TotalSize, $FilePath, $FileString, $TorrentID ); G::$Cache->delete_value("torrents_details_$GroupID"); } G::$DB->set_query_id($QueryID); } /** * Return UTF-8 encoded string to use as file delimiter in torrent file lists */ public static function filelist_delim() { static $FilelistDelimUTF8; if (isset($FilelistDelimUTF8)) { return $FilelistDelimUTF8; } return $FilelistDelimUTF8 = utf8_encode(chr(self::FILELIST_DELIM)); } /** * Create a string that contains file info in a format that's easy to use for Sphinx * * @param array $File (File size, File name) * @return string with the format .EXT sSIZEs NAME DELIMITER */ public static function filelist_format_file($File) { list($Size, $Name) = $File; $Name = Format::make_utf8(strtr($Name, "\n\r\t", ' ')); $ExtPos = strrpos($Name, '.'); // Should not be $ExtPos !== false. Extensionless files that start with a . should not get extensions $Ext = ($ExtPos ? trim(substr($Name, $ExtPos + 1)) : ''); return sprintf("%s s%ds %s %s", ".$Ext", $Size, $Name, self::filelist_delim()); } /** * Create a string that contains file info in the old format for the API * * @param string $File string with the format .EXT sSIZEs NAME DELIMITER * @return string with the format NAME{{{SIZE}}} */ public static function filelist_old_format($File) { $File = self::filelist_get_file($File); return $File['name'] . '{{{' . $File['size'] . '}}}'; } /** * Translate a formatted file info string into a more useful array structure * * @param string $File string with the format .EXT sSIZEs NAME DELIMITER * @return file info array with the keys 'ext', 'size' and 'name' */ public static function filelist_get_file($File) { // Need this hack because filelists are always display_str()ed $DelimLen = strlen(display_str(self::filelist_delim())) + 1; list($FileExt, $Size, $Name) = explode(' ', $File, 3); if ($Spaces = strspn($Name, ' ')) { $Name = str_replace(' ', ' ', substr($Name, 0, $Spaces)) . substr($Name, $Spaces); } return array( 'ext' => $FileExt, 'size' => substr($Size, 1, -1), 'name' => substr($Name, 0, -$DelimLen) ); } /** * Format the information about a torrent. * @param $Data an array a subset of the following keys: * Format, Encoding, HasLog, LogScore HasCue, Media, Scene, RemasterYear * RemasterTitle, FreeTorrent, PersonalFL * @param boolean $ShowMedia if false, Media key will be omitted * @param boolean $ShowEdition if false, RemasterYear/RemasterTitle will be omitted */ public static function torrent_info($Data, $ShowMedia = true, $ShowEdition = false, $HTMLy = true) { # Main torrent search results info! $Info = []; # Platform if ($ShowMedia && !empty($Data['Media'])) { $Info[] = ($HTMLy) ? '' . display_str($Data['Media']) . '' : display_str($Data['Media']); } # Format if (!empty($Data['Container'])) { $Info[] = ($HTMLy) ? '' . display_str($Data['Container']) . '' : display_str($Data['Container']); } # Archive if (!empty($Data['Archive'])) { # todo: Search on archives, lowest priority $Info[] = display_str($Data['Archive']); } # Resolution if (!empty($Data['Resolution'])) { $Info[] = ($HTMLy) ? '' . display_str($Data['Resolution']) . '' : display_str($Data['Resolution']); } # License if (!empty($Data['Codec'])) { $Info[] = ($HTMLy) ? '' . display_str($Data['Codec']) . '' : display_str($Data['Codec']); } # Alignned/Annotated if ($Data['Censored'] === 1) { $Info[] = ($HTMLy) ? 'Aligned' : 'Aligned'; } else { $Info[] = ($HTMLy) ? 'Not Aligned' : 'Not Aligned'; } /* if (!empty($Data['Version'])) { $Info[] = $Data['Version']; } */ if ($Data['IsLeeching']) { $Info[] = $HTMLy ? Format::torrent_label('Leeching', 'important_text_semi') : 'Leeching'; } elseif ($Data['IsSeeding']) { $Info[] = $HTMLy ? Format::torrent_label('Seeding', 'important_text_alt') : 'Seeding'; } elseif ($Data['IsSnatched']) { $Info[] = $HTMLy ? Format::torrent_label('Snatched', 'bold') : 'Snatched'; } if ($Data['FreeTorrent'] === '1') { if ($Data['FreeLeechType'] === '3') { if ($Data['ExpiryTime']) { $Info[] = ($HTMLy ? Format::torrent_label('Freeleech', 'important_text_alt') : 'Freeleech') . ($HTMLy ? " (" : " (").str_replace(['month','week','day','hour','min','s'], ['m','w','d','h','m',''], time_diff(max(strtotime($Data['ExpiryTime']), time()), 1, false)).($HTMLy ? ")" : ")"); } else { $Info[] = $HTMLy ? Format::torrent_label('Freeleech', 'important_text_alt') : 'Freeleech'; } } else { $Info[] = $HTMLy ? Format::torrent_label('Freeleech', 'important_text_alt') : 'Freeleech'; } } if ($Data['FreeTorrent'] == '2') { $Info[] = $HTMLy ? Format::torrent_label('Neutral Leech', 'bold') : 'Neutral Leech'; } if ($Data['PersonalFL']) { $Info[] = $HTMLy ? Format::torrent_label('Personal Freeleech', 'important_text_alt') : 'Personal Freeleech'; } return implode(' | ', $Info); } /** * Will freeleech / neutral leech / normalise a set of torrents * * @param array $TorrentIDs An array of torrent IDs to iterate over * @param int $FreeNeutral 0 = normal, 1 = fl, 2 = nl * @param int $FreeLeechType 0 = Unknown, 1 = Staff picks, 2 = Perma-FL (Toolbox, etc.), 3 = Vanity House */ public static function freeleech_torrents($TorrentIDs, $FreeNeutral = 1, $FreeLeechType = 0, $Announce = true) { if (!is_array($TorrentIDs)) { $TorrentIDs = array($TorrentIDs); } $QueryID = G::$DB->get_query_id(); G::$DB->query(" UPDATE torrents SET FreeTorrent = '$FreeNeutral', FreeLeechType = '$FreeLeechType' WHERE ID IN (".implode(', ', $TorrentIDs).')'); G::$DB->query(' SELECT ID, GroupID, info_hash FROM torrents WHERE ID IN ('.implode(', ', $TorrentIDs).') ORDER BY GroupID ASC'); $Torrents = G::$DB->to_array(false, MYSQLI_NUM, false); $GroupIDs = G::$DB->collect('GroupID'); G::$DB->set_query_id($QueryID); foreach ($Torrents as $Torrent) { list($TorrentID, $GroupID, $InfoHash) = $Torrent; Tracker::update_tracker('update_torrent', array('info_hash' => rawurlencode($InfoHash), 'freetorrent' => $FreeNeutral)); G::$Cache->delete_value("torrent_download_$TorrentID"); Misc::write_log((G::$LoggedUser['Username']??'System')." marked torrent $TorrentID freeleech type $FreeLeechType"); Torrents::write_group_log($GroupID, $TorrentID, (G::$LoggedUser['ID']??0), "marked as freeleech type $FreeLeechType", 0); if ($Announce && ($FreeLeechType === 1 || $FreeLeechType === 3)) { send_irc(ANNOUNCE_CHAN, 'FREELEECH - '.site_url()."torrents.php?id=$GroupID / ".site_url()."torrents.php?action=download&id=$TorrentID"); } } foreach ($GroupIDs as $GroupID) { Torrents::update_hash($GroupID); } } /** * Convenience function to allow for passing groups to Torrents::freeleech_torrents() * * @param array $GroupIDs the groups in question * @param int $FreeNeutral see Torrents::freeleech_torrents() * @param int $FreeLeechType see Torrents::freeleech_torrents() */ public static function freeleech_groups($GroupIDs, $FreeNeutral = 1, $FreeLeechType = 0) { $QueryID = G::$DB->get_query_id(); if (!is_array($GroupIDs)) { $GroupIDs = [$GroupIDs]; } G::$DB->query(' SELECT ID FROM torrents WHERE GroupID IN ('.implode(', ', $GroupIDs).')'); if (G::$DB->has_results()) { $TorrentIDs = G::$DB->collect('ID'); Torrents::freeleech_torrents($TorrentIDs, $FreeNeutral, $FreeLeechType); } G::$DB->set_query_id($QueryID); } /** * Check if the logged in user has an active freeleech token * * @param int $TorrentID * @return true if an active token exists */ public static function has_token($TorrentID) { if (empty(G::$LoggedUser)) { return false; } static $TokenTorrents; $UserID = G::$LoggedUser['ID']; if (!isset($TokenTorrents)) { $TokenTorrents = G::$Cache->get_value("users_tokens_$UserID"); if ($TokenTorrents === false) { $QueryID = G::$DB->get_query_id(); G::$DB->query(" SELECT TorrentID FROM users_freeleeches WHERE UserID = ? AND Expired = 0", $UserID); $TokenTorrents = array_fill_keys(G::$DB->collect('TorrentID', false), true); G::$DB->set_query_id($QueryID); G::$Cache->cache_value("users_tokens_$UserID", $TokenTorrents); } } return isset($TokenTorrents[$TorrentID]); } /** * Check if the logged in user can use a freeleech token on this torrent * * @param int $Torrent * @return boolen True if user is allowed to use a token */ public static function can_use_token($Torrent) { if (empty(G::$LoggedUser)) { return false; } return (G::$LoggedUser['FLTokens'] > 0 && $Torrent['Size'] <= 10737418240 && !$Torrent['PersonalFL'] && empty($Torrent['FreeTorrent']) && G::$LoggedUser['CanLeech'] == '1'); } /** * Build snatchlists and check if a torrent has been snatched * if a user has the 'ShowSnatched' option enabled * @param int $TorrentID * @return bool */ public static function has_snatched($TorrentID) { if (empty(G::$LoggedUser) || !isset(G::$LoggedUser['ShowSnatched']) || !G::$LoggedUser['ShowSnatched']) { return false; } $UserID = G::$LoggedUser['ID']; $Buckets = 64; $LastBucket = $Buckets - 1; $BucketID = $TorrentID & $LastBucket; static $SnatchedTorrents = [], $UpdateTime = []; if (empty($SnatchedTorrents)) { $SnatchedTorrents = array_fill(0, $Buckets, false); $UpdateTime = G::$Cache->get_value("users_snatched_{$UserID}_time"); if ($UpdateTime === false) { $UpdateTime = array( 'last' => 0, 'next' => 0); } } elseif (isset($SnatchedTorrents[$BucketID][$TorrentID])) { return true; } // Torrent was not found in the previously inspected snatch lists $CurSnatchedTorrents =& $SnatchedTorrents[$BucketID]; if ($CurSnatchedTorrents === false) { $CurTime = time(); // This bucket hasn't been checked before $CurSnatchedTorrents = G::$Cache->get_value("users_snatched_{$UserID}_$BucketID", true); if ($CurSnatchedTorrents === false || $CurTime > $UpdateTime['next']) { $Updated = []; $QueryID = G::$DB->get_query_id(); if ($CurSnatchedTorrents === false || $UpdateTime['last'] == 0) { for ($i = 0; $i < $Buckets; $i++) { $SnatchedTorrents[$i] = []; } // Not found in cache. Since we don't have a suitable index, it's faster to update everything G::$DB->query(" SELECT fid FROM xbt_snatched WHERE uid = ?", $UserID); while (list($ID) = G::$DB->next_record(MYSQLI_NUM, false)) { $SnatchedTorrents[$ID & $LastBucket][(int)$ID] = true; } $Updated = array_fill(0, $Buckets, true); } elseif (isset($CurSnatchedTorrents[$TorrentID])) { // Old cache, but torrent is snatched, so no need to update return true; } else { // Old cache, check if torrent has been snatched recently G::$DB->query(" SELECT fid FROM xbt_snatched WHERE uid = ? AND tstamp >= ?", $UserID, $UpdateTime['last']); while (list($ID) = G::$DB->next_record(MYSQLI_NUM, false)) { $CurBucketID = $ID & $LastBucket; if ($SnatchedTorrents[$CurBucketID] === false) { $SnatchedTorrents[$CurBucketID] = G::$Cache->get_value("users_snatched_{$UserID}_$CurBucketID", true); if ($SnatchedTorrents[$CurBucketID] === false) { $SnatchedTorrents[$CurBucketID] = []; } } $SnatchedTorrents[$CurBucketID][(int)$ID] = true; $Updated[$CurBucketID] = true; } } G::$DB->set_query_id($QueryID); for ($i = 0; $i < $Buckets; $i++) { if (isset($Updated[$i])) { G::$Cache->cache_value("users_snatched_{$UserID}_$i", $SnatchedTorrents[$i], 0); } } $UpdateTime['last'] = $CurTime; $UpdateTime['next'] = $CurTime + self::SNATCHED_UPDATE_INTERVAL; G::$Cache->cache_value("users_snatched_{$UserID}_time", $UpdateTime, 0); } } return isset($CurSnatchedTorrents[$TorrentID]); } public static function is_seeding($TorrentID) { if (empty(G::$LoggedUser) || !isset(G::$LoggedUser['ShowSnatched']) || !G::$LoggedUser['ShowSnatched']) { return false; } $UserID = G::$LoggedUser['ID']; $Buckets = 64; $LastBucket = $Buckets - 1; $BucketID = $TorrentID & $LastBucket; static $SeedingTorrents = [], $UpdateTime = []; if (empty($SeedingTorrents)) { $SeedingTorrents = array_fill(0, $Buckets, false); $UpdateTime = G::$Cache->get_value("users_seeding_{$UserID}_time"); if ($UpdateTime === false) { $UpdateTime = array( 'last' => 0, 'next' => 0); } } elseif (isset($SeedingTorrents[$BucketID][$TorrentID])) { return true; } // Torrent was not found in the previously inspected seeding lists $CurSeedingTorrents =& $SeedingTorrents[$BucketID]; if ($CurSeedingTorrents === false) { $CurTime = time(); // This bucket hasn't been checked before $CurSeedingTorrents = G::$Cache->get_value("users_seeding_{$UserID}_$BucketID", true); if ($CurSeedingTorrents === false || $CurTime > $UpdateTime['next']) { $Updated = []; $QueryID = G::$DB->get_query_id(); if ($CurSeedingTorrents === false || $UpdateTime['last'] == 0) { for ($i = 0; $i < $Buckets; $i++) { $SeedingTorrents[$i] = []; } // Not found in cache. Since we don't have a suitable index, it's faster to update everything G::$DB->query(" SELECT fid FROM xbt_files_users WHERE uid = ? AND active = 1 AND Remaining = 0", $UserID); while (list($ID) = G::$DB->next_record(MYSQLI_NUM, false)) { $SeedingTorrents[$ID & $LastBucket][(int)$ID] = true; } $Updated = array_fill(0, $Buckets, true); } elseif (isset($CurSeedingTorrents[$TorrentID])) { // Old cache, but torrent is seeding, so no need to update return true; } else { // Old cache, check if torrent has been seeding recently G::$DB->query(" SELECT fid FROM xbt_files_users WHERE uid = ? AND active = 1 AND Remaining = 0 AND mtime >= ?", $UserID, $UpdateTime['last']); while (list($ID) = G::$DB->next_record(MYSQLI_NUM, false)) { $CurBucketID = $ID & $LastBucket; if ($SeedingTorrents[$CurBucketID] === false) { $SeedingTorrents[$CurBucketID] = G::$Cache->get_value("users_seeding_{$UserID}_$CurBucketID", true); if ($SeedingTorrents[$CurBucketID] === false) { $SeedingTorrents[$CurBucketID] = []; } } $SeedingTorrents[$CurBucketID][(int)$ID] = true; $Updated[$CurBucketID] = true; } } G::$DB->set_query_id($QueryID); for ($i = 0; $i < $Buckets; $i++) { if (isset($Updated[$i])) { G::$Cache->cache_value("users_seeding_{$UserID}_$i", $SeedingTorrents[$i], 3600); } } $UpdateTime['last'] = $CurTime; $UpdateTime['next'] = $CurTime + self::SNATCHED_UPDATE_INTERVAL; G::$Cache->cache_value("users_seeding_{$UserID}_time", $UpdateTime, 3600); } } return isset($CurSeedingTorrents[$TorrentID]); } public static function is_leeching($TorrentID) { if (empty(G::$LoggedUser) || !isset(G::$LoggedUser['ShowSnatched']) || !G::$LoggedUser['ShowSnatched']) { return false; } $UserID = G::$LoggedUser['ID']; $Buckets = 64; $LastBucket = $Buckets - 1; $BucketID = $TorrentID & $LastBucket; static $LeechingTorrents = [], $UpdateTime = []; if (empty($LeechingTorrents)) { $LeechingTorrents = array_fill(0, $Buckets, false); $UpdateTime = G::$Cache->get_value("users_leeching_{$UserID}_time"); if ($UpdateTime === false) { $UpdateTime = array( 'last' => 0, 'next' => 0); } } elseif (isset($LeechingTorrents[$BucketID][$TorrentID])) { return true; } // Torrent was not found in the previously inspected snatch lists $CurLeechingTorrents =& $LeechingTorrents[$BucketID]; if ($CurLeechingTorrents === false) { $CurTime = time(); // This bucket hasn't been checked before $CurLeechingTorrents = G::$Cache->get_value("users_leeching_{$UserID}_$BucketID", true); if ($CurLeechingTorrents === false || $CurTime > $UpdateTime['next']) { $Updated = []; $QueryID = G::$DB->get_query_id(); if ($CurLeechingTorrents === false || $UpdateTime['last'] == 0) { for ($i = 0; $i < $Buckets; $i++) { $LeechingTorrents[$i] = []; } // Not found in cache. Since we don't have a suitable index, it's faster to update everything G::$DB->query(" SELECT fid FROM xbt_files_users WHERE uid = ? AND active = 1 AND Remaining > 0", $UserID); while (list($ID) = G::$DB->next_record(MYSQLI_NUM, false)) { $LeechingTorrents[$ID & $LastBucket][(int)$ID] = true; } $Updated = array_fill(0, $Buckets, true); } elseif (isset($CurLeechingTorrents[$TorrentID])) { // Old cache, but torrent is leeching, so no need to update return true; } else { // Old cache, check if torrent has been leeching recently G::$DB->query(" SELECT fid FROM xbt_files_users WHERE uid = ? AND active = 1 AND Remaining > 0 AND mtime >= ?", $UserID, $UpdateTime['last']); while (list($ID) = G::$DB->next_record(MYSQLI_NUM, false)) { $CurBucketID = $ID & $LastBucket; if ($LeechingTorrents[$CurBucketID] === false) { $LeechingTorrents[$CurBucketID] = G::$Cache->get_value("users_leeching_{$UserID}_$CurBucketID", true); if ($LeechingTorrents[$CurBucketID] === false) { $LeechingTorrents[$CurBucketID] = []; } } $LeechingTorrents[$CurBucketID][(int)$ID] = true; $Updated[$CurBucketID] = true; } } G::$DB->set_query_id($QueryID); for ($i = 0; $i < $Buckets; $i++) { if (isset($Updated[$i])) { G::$Cache->cache_value("users_leeching_{$UserID}_$i", $LeechingTorrents[$i], 3600); } } $UpdateTime['last'] = $CurTime; $UpdateTime['next'] = $CurTime + self::SNATCHED_UPDATE_INTERVAL; G::$Cache->cache_value("users_leeching_{$UserID}_time", $UpdateTime, 3600); } } return isset($CurLeechingTorrents[$TorrentID]); } /* public static function is_seeding_or_leeching($TorrentID) { if (empty(G::$LoggedUser)) return false; $UserID = G::$LoggedUser['ID']; $Result = array("IsSeeding" => false, "IsLeeching" => false); $QueryID = G::$DB->get_query_id(); G::$DB->query(" SELECT Remaining FROM xbt_files_users WHERE fid = $TorrentID AND uid = $UserID AND active = 1"); if (G::$DB->has_results()) { while (($Row = G::$DB->next_record(MYSQLI_ASSOC, true)) && !($Result['IsSeeding'] && $Result['IsLeeching'])) { if ($Row['Remaining'] == 0) $Result['IsSeeding'] = true; if ($Row['Remaining'] > 0) $Result['IsLeeching'] = true; } } G::$DB->set_query_id($QueryID); return $Result; } */ /** * Change the schedule for when the next update to a user's cached snatch list should be performed. * By default, the change will only be made if the new update would happen sooner than the current * @param int $Time Seconds until the next update * @param bool $Force Whether to accept changes that would push back the update */ public static function set_snatch_update_time($UserID, $Time, $Force = false) { if (!$UpdateTime = G::$Cache->get_value("users_snatched_{$UserID}_time")) { return; } $NextTime = time() + $Time; if ($Force || $NextTime < $UpdateTime['next']) { // Skip if the change would delay the next update $UpdateTime['next'] = $NextTime; G::$Cache->cache_value("users_snatched_{$UserID}_time", $UpdateTime, 0); } } // Some constants for self::display_string's $Mode parameter const DISPLAYSTRING_HTML = 1; // Whether or not to use HTML for the output (e.g. VH tooltip) const DISPLAYSTRING_ARTISTS = 2; // Whether or not to display artists const DISPLAYSTRING_YEAR = 4; // Whether or not to display the group's year const DISPLAYSTRING_VH = 8; // Whether or not to display the VH flag const DISPLAYSTRING_RELEASETYPE = 16; // Whether or not to display the release type const DISPLAYSTRING_LINKED = 33; // Whether or not to link artists and the group // 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 // Keep this in mind when defining presets below! // Presets to facilitate the use of $Mode const DISPLAYSTRING_DEFAULT = 63; // HTML|ARTISTS|YEAR|VH|RELEASETYPE|LINKED = 63 const DISPLAYSTRING_SHORT = 6; // Very simple format, only artists and year, no linking (e.g. for forum thread titles) /** * Return the display string for a given torrent group $GroupID. * @param int $GroupID * @return string */ public static function display_string($GroupID, $Mode = self::DISPLAYSTRING_DEFAULT) { #global $ReleaseTypes; // I hate this $GroupInfo = self::get_groups(array($GroupID), true, true, false)[$GroupID]; $ExtendedArtists = $GroupInfo['ExtendedArtists']; if ($Mode & self::DISPLAYSTRING_ARTISTS) { if (!empty($ExtendedArtists[1]) || !empty($ExtendedArtists[4]) || !empty($ExtendedArtists[5]) || !empty($ExtendedArtists[6]) ) { unset($ExtendedArtists[2], $ExtendedArtists[3]); $DisplayName = Artists::display_artists($ExtendedArtists, ($Mode & self::DISPLAYSTRING_LINKED)); } else { $DisplayName = ''; } } if ($Mode & self::DISPLAYSTRING_LINKED) { $DisplayName .= "$GroupInfo[Name]"; } else { $DisplayName .= $GroupInfo['Name']; } if (($Mode & self::DISPLAYSTRING_YEAR) && $GroupInfo['Year'] > 0) { $DisplayName .= " [$GroupInfo[Year]]"; } if (($Mode & self::DISPLAYSTRING_RELEASETYPE) && $GroupInfo['ReleaseType'] > 0) { $DisplayName .= ' ['.$ReleaseTypes[$GroupInfo['ReleaseType']].']'; } return $DisplayName; } public static function edition_string(array $Torrent, array $Group) { /* $AddExtra = ' | '; # breaking $EditionName = 'Original Release'; $EditionName .= $AddExtra . display_str($Torrent['Media']); return $EditionName; */ } // Used to get reports info on a unison cache in both browsing pages and torrent pages. public static function get_reports($TorrentID) { $Reports = G::$Cache->get_value("reports_torrent_$TorrentID"); if ($Reports === false) { $QueryID = G::$DB->get_query_id(); G::$DB->query(" SELECT ID, ReporterID, Type, UserComment, ReportedTime FROM reportsv2 WHERE TorrentID = ? AND Status != 'Resolved'", $TorrentID); $Reports = G::$DB->to_array(false, MYSQLI_ASSOC, false); G::$DB->set_query_id($QueryID); G::$Cache->cache_value("reports_torrent_$TorrentID", $Reports, 0); } if (!check_perms('admin_reports')) { $Return = []; foreach ($Reports as $Report) { if ($Report['Type'] !== 'edited') { $Return[] = $Report; } } return $Return; } return $Reports; } }