- PVSM.RU - https://www.pvsm.ru -

Как запустить собственный торрент-поисковик на базе RuTracker?

Я максимально постараюсь писать без «воды». Минимум лишней отвлекающей информации и разглагольствований. Максимум полезной информации и рабочего кода. Я не буду поднимать вопрос зачем кому-то собственный торрент-поисковик на базе RuTracker. И я не считаю себя гуру программирования. Мы просто сделаем этот сайт вместе. Будем использовать Apache+PHP, MySQL и Sphinx. Сразу предупрежу, что на минимальном виртуальном хостинге [1] сайт будет работать совсем не быстро.

image

База данных

Для начала нам надо взять саму базу. RuTracker каждый месяц выкладывает дамп своих торрентов здесь [2]. Скачиваем, распаковываем и видим два десятка файлов CSV.

image

Нам нужны только те, в которых есть информация о торрентах – остальные удаляем. В файле «category_info.csv» — подсказка для тех, кому не хочется открывать каждый файл (удалить: «category_1.csv», «category_4.csv», «category_36.csv»). Открываем любой из оставшихся файлов и видим такую структуру (я сразу заменил символ «;» на новую строку, что бы было визуально удобнее):

«1568» ID раздела на RuTracker
«Кулинария» Название раздела
«63629» ID темы на RuTracker
«F7D7BE97A818CCDFA072C42348EB669F7883888D» Hash торрента
"(Кулинария) Вкусные истории 1" Название торрента
«729927066» Размер раздачи в байтах
«2006-08-21 10:00:22» Дата публикации раздачи

Теперь мы добавим всю информацию в базу данных. Используем MySQL, как самую распространённую БД. У меня получилась вот такая таблица (обратите внимание: столбец «hash» — уникальный, все текстовые данные в кодировке utf8):

Таблица SQL

CREATE TABLE IF NOT EXISTS `torrents` (
  `id` int(11) NOT NULL,
  `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,
  `hash` varchar(40) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,
  `date` date NOT NULL,
  `size` int(11) NOT NULL,
  `topic_id` int(11) NOT NULL,
  `cat_id` int(11) NOT NULL,
  `cat_name` varchar(120) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL
) ENGINE=MyISAM DEFAULT CHARSET=utf32 COLLATE=utf32_bin;

ALTER TABLE `torrents`
  ADD PRIMARY KEY (`id`),
  ADD UNIQUE KEY `hash` (`hash`);

ALTER TABLE `torrents`
  MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;

Затем закачиваем все файлы CSV в одну папку (например, назовем её «db») на сервере. Добавление информации о торрентах в БД осуществим с помощью не сложного скрипта, представленного ниже. Его тоже необходимо закачать в ту же папку где находятся исходные CSV-файлы.

Файл insert_to_db.php

<?
//Ограничиваем время выполнения скрипта 3-мя минутами
set_time_limit(180);


//Подключаемся к MySQL, при неудаче выводим ошибку
mysql_connect("localhost", "torrent", "password") or die("Could not connect to MySQL");


//Выбираем БД, при неудаче выводим ошибку
mysql_select_db("torrent") or die("Could not select database");


//Переводим все общение с БД в кодировку utf8
mysql_query("SET NAMES utf8");


//Открываем файл указанный в url переменной "f"
$fp = fopen($_GET[f], "r");


//Запускаем цикл до конца строк в файле
while (!feof($fp)) {
        //Считываем строку (да, функцию trim() выполнять не обязательно, но у каждого программиста свои "тараканы")
        $tmp = trim(fgets($fp));
        
        
        //Преобразуем строку в массив. За разделитель используем ";"
        $torrent = explode('";"', $tmp);
        
        
        //В первом и последнем элементе удаляем лишние символы "
        $torrent[0] = substr($torrent[0], 1);
        $torrent[6] = substr($torrent[6], 0, (strlen($torrent[6]) - 1));
        
        
        //Если раскомментировать следующую строку, то можно увидеть как распарсился первый торрент в файле
        //print '<pre>'; print_r($torrent); exit();
        
        
        //Вставляем данные текущего торрента в таблицу
        mysql_query("INSERT INTO `torrents` 
            (`name`,
            `hash`,
            `date`,
            `size`,
            `topic_id`,
            `cat_id`,
            `cat_name`) 
          VALUES 
            ('" . mysql_real_escape_string($torrent[4]) . "',
            '" . $torrent[3] . "',
            '" . $torrent[6] . "',
            '" . $torrent[5] . "',
            '" . $torrent[2] . "',
            '" . $torrent[0] . "',
            '" . mysql_real_escape_string($torrent[1]) . "')
        ");
}
//Закрываем файл
fclose($fp);

//Выводим сообщение о завершении работы
print 'complete: ' . $_GET[f];
?>

Открываем браузер, открываем url «http://site.ru/db/insert_to_db.php?f=category_10.csv». Проделываем тоже самое с каждым файлом CSV. Да, все это можно было автоматизировать, но я специально написал так, что бы было максимально всё понятно. После этих действий в нашей таблице оказалось чуть больше 1,6 миллиона записей. Не маленькая такая база. Поиск MySQL с таким объемом данных не справится, так что поручим эту задачу Sphinx.

image

Sphinx

Установка Sphinx на различные системы производится разными способами. Все зависит от операционной системы и железа. Это тема заслуживает отдельной статьи. Но есть очень много отличных мануалов в Интернете. На русском языке тоже. Сейчас же мы займемся настройкой конфигурационного файла для Sphinx. Создаем в корневом каталоге сайта директорию, допустим, cache. Здесь будут хранится все файлы индекса Sphinx для нашего сайта. Загружаем в эту папку файл конфигурации (листинг приведен ниже).

Файл torrents.conf

# Настройка источника откуда берутся данные
source torrentz
{
        # Подключаемся к БД
        type = mysql
        sql_host = localhost
        sql_user = torrent
        sql_pass = password
        sql_db = torrent
        sql_port = 3306

        # Переводим все общение с БД в кодировку utf8
        sql_query_pre = SET NAMES utf8
        sql_query_pre = SET CHARACTER SET utf8

        # Запрос данных для индексации
        sql_query = SELECT id, name FROM torrents

        # Время (в миллисекундах) паузы перед посылкой запроса БД. Используется для медленных и загруженных серверов
        sql_ranged_throttle = 0
}


# Настройка индекса. Более подробно все описано в документации Sphinx
index torrentz
{
        # Выбор источника
        source = torrentz

        # Путь до файлов индекса
        path = /home/rutr/rutracker.online/www/cache/

        # Способ хранения индекса 
        docinfo = extern

        # Использование английской и русской морфологии
        morphology = stem_enru

        # Минимальная длина индексируемого слова
        min_word_len = 2

        # Установка кодировки
        charset_type = utf-8

        # Символы
        charset_table = 0..9, A..Z->a..z, _, a..z, U+410..U+42F->U+430..U+44F, U+430..U+44F

        # Минимальная длина инфикса 
        min_infix_len = 2

        # Использовать оператор усечения "*"
        enable_star = 1
}


# Настройка индексатора
indexer
{
        # Лимит используемой оперативной памяти
        mem_limit = 32M
}


# Настройка поискового демона
searchd
{
		# Указываем порт на который сайт будет отдавать запросы на поиск
		listen = 127.0.0.1:3312

        # Лог
        log = /home/rutr/rutracker.online/www/cache/searchd.log

        # Лог запросов
        query_log = /home/rutr/rutracker.online/www/cache/query.log

        # Таймаут на соединение с сервером
        read_timeout = 5

        # Максимальное кол-во потомков от процесса
        max_children = 30

        # Путь до pid-файла
        pid_file = /home/rutr/rutracker.online/www/cache/searchd.pid

        # Максимальное кол-во результатов выдачи
        max_matches = 1000
}

Подключаемся к серверу через ssh. Для того что бы Sphinx смог искать по нашей базе, надо подготовить индекс. Выполняем команду:

indexer --config /home/rutr/rutracker.online/www/cache/torrents.conf –all

Sphinx некоторое время будет проводить индекс базы данных. Длительность зависит от мощности сервера. В моем случае индексирование заняло около 10 минут.

image

После окончания индексирования, проверим все ли нормально прошло. Для этого выполним поиск через консоль с помощью команды (поисковая фраза пишется после указания файла конфига):

search --config /home/rutr/rutracker.online/www/cache/torrents.conf morrowind mod

image

Если вы увидели что-то похожее на верхний скриншот, значит индексирование прошло успешно. Если ничего не найдено, то не нужно запускать следующую команду. Для запуска демона поиска Sphinx выполняем команду:

searchd --config /home/rutr/rutracker.online/www/cache/torrents.conf 

image

Обратите внимание, что демон нужно запускать после каждой перезагрузки системы. Для выключения демона (если нужно) добавляем «--stop» в конце вышеуказанной команды.

Web

Я не долго думал какой фреймворк использовать для web-интерфейса. Требования простые: простота в использовании, адаптивный дизайн и поддержка всех современных браузеров. Под это отлично подходит, пусть и немного надоевший, Bootstrap. Дистрибутив качать не обязательно, можно подключить файл стилей онлайн. Главная страница на чистом HTML, без использования PHP. Комментарии к коду, думаю, будут излишни.

Файл index.php

<!DOCTYPE html>
<html lang="ru">
	<head>
		<meta charset="utf-8">
		<meta http-equiv="X-UA-Compatible" content="IE=edge">
		<meta name="viewport" content="width=device-width, initial-scale=1">
		<link rel="icon" href="/favicon.ico">
		<title>Зеркало раздач RuTracker</title>
		<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
		<style type="text/css">
			.inCenter {
			    margin: auto;
			    position: absolute;
			    top: 0;
			    left: 0;
			    bottom: 0;
			    right: 0;
			}
			.inCenter.isResp {
			    width: 50%;
			    height: 50%;
			    min-width: 400px;
			    max-width: 800px;
			    padding: 40px;
			}
		</style>
	</head>
	<body>
		<div class="container">
			<div class="row">
				<div class="inCenter isResp">
					<div class="col-sm-12 col-md-10 col-md-offset-1">
						<form action="search.php" method="GET">
							<div class="form-group text-center">
								<h1>Зеркало раздач RuTracker</h1>
							</div>
							<div class="form-group input-group">
								<input class="form-control input-lg" type="text" name="q" placeholder=""/>      
								<span class="input-group-btn">
									<button class="btn btn-primary input-lg" type="submit"><i class="glyphicon glyphicon-search"></i></button>
								</span>	                
							</div>
						</form>
					</div>
				</div>
			</div>
		</div>
	</body>
</html>

Дизайн главной страницы получился очень минималистичным и максимально функциональным.

image

Скрипт поиска будет уже интереснее. Для начала нам нужен API Sphinx на PHP. Последнюю версию можно взять здесь [3]. Кратко расскажу, как работает скрипт поиска, а подробнее уже в листинге. Подключаем файл для работы с API, настраиваем поиск, ищем, выкладываем результаты поиска в удобном виде. Скачать торрент можно будет прямо из поиска, без дополнительных кликов.

Файл search.php

<?
//Фильтруем и форматируем запрос
$q=trim(urldecode($_GET[q]));


//Если нет запроса на поиск, то делаем редирект на главную страницу
if (empty($q)) {header("Location: /"); exit();}


//Если запрос есть, то...
//Подключаемся к MySQL, при неудаче выводим ошибку
mysql_connect("localhost", "torrent", "password") or die("Could not connect to MySQL");


//Выбираем БД, при неудаче выводим ошибку
mysql_select_db("torrent") or die("Could not select database");


//Переводим все общение с БД в кодировку utf8
mysql_query("SET NAMES utf8");

//Подключаем API Sphinx
include("sphinxapi.php");


//Создаем объект Sphinx 
$sphinx=new SphinxClient();


//Подключаемся к Sphinx-серверу. Порт мы указываем в файле "torrents.conf"
$sphinx->SetServer('localhost', 3312);


//Ищем совадение по любомым словом
$sphinx->SetMatchMode(SPH_MATCH_ANY);


//Сортируем результаты по релевантности
$sphinx->SetSortMode(SPH_SORT_RELEVANCE);


//Выводим 50 результатов начиная с первого.
$sphinx->SetLimits(0, 50);

    
//Запускаем поиск (* - использование всех индексов в файле "torrents.conf", но он у нас один: torrentz)
$torrents=$sphinx->Query($q, '*');


//Если раскомментировать следующие две строки, то можно увидеть ошибки и как отработал поиск
//print $sphinx->getLastError();
//print '<br><pre>'; print_r($torrents); exit();



//Функция перевода байтов в килобайты, мегабайты и тд. Пригодится нам ниже. Описывать её прицип не имеем смысла - чистая арифметика. 
function bytesToSize($bytes, $precision = 0)
{   
        $kilobyte = 1024;
        $megabyte = $kilobyte * 1024;
        $gigabyte = $megabyte * 1024;
        $terabyte = $gigabyte * 1024;

        
        if (($bytes >= 0) && ($bytes < $kilobyte)) {return $bytes . ' B';} 
        elseif (($bytes >= $kilobyte) && ($bytes < $megabyte)) {return round($bytes / $kilobyte, $precision) . ' Kb';}
        elseif (($bytes >= $megabyte) && ($bytes < $gigabyte)) {return round($bytes / $megabyte, $precision) . ' Mb';}
        elseif (($bytes >= $gigabyte) && ($bytes < $terabyte)) {return round($bytes / $gigabyte, $precision) . ' Gb';}
        elseif ($bytes >= $terabyte) {return round($bytes / $terabyte, $precision) . ' Tb';} 
        else {return $bytes . ' B';}
    }    


?>
<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="utf-8">
		<meta http-equiv="X-UA-Compatible" content="IE=edge">
		<meta name="viewport" content="width=device-width, initial-scale=1">
		<link rel="icon" href="/favicon.ico">
		<title><?=htmlspecialchars($q)?></title>
		<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
		<style type="text/css">
			body
			{
			padding-top: 80px;
			padding-bottom: 20px;
			}
		</style>
	</head>
	<body>
		<nav class="navbar navbar-default navbar-fixed-top">
			<div class="container">
				<div class="navbar-header">
					<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
					<span class="sr-only">Навигация</span>
					<span class="icon-bar"></span>
					<span class="icon-bar"></span>
					<span class="icon-bar"></span>
					</button>
					<a class="navbar-brand" href="/">Зеркало раздач RuTracker</a>
				</div>
				<div id="navbar" class="navbar-collapse collapse">
					<form action="/search.php" method="GET" class="navbar-form navbar-left">
						<div class="form-group input-group">
							<input type="text" placeholder="" value="<?=htmlspecialchars($q)?>" class="form-control" name="q"> 
							<span class="input-group-btn">
							<button class="btn btn-primary" type="submit"><i class="glyphicon glyphicon-search"></i></button>
							</span>  
						</div>
					</form>
				</div>
				<!--/.navbar-collapse -->
			</div>
		</nav>
		<div class="container">
			<h1><?=htmlspecialchars($q)?></h1>
			<table class="table table-striped">
				<caption>Всего найдено: <?=$torrents[total_found]?></caption>
				<tbody>
					<?
						//Преобразовывем ключи в полученном массиве результатов поиска в массив
					    $ids = array_keys($torrents[matches]);


					    //Собираем массив с id-ми записей в понятный для SQL формат
					    $ids = implode(',', $ids);


					    //Пишем SQL запрос для выборки данных по результатам поиска
					    $sql="SELECT 
						    	`id`,
						    	`name`,
						    	`hash`,
						    	`date`,
						    	`size` 
					    	FROM `torrents` 
					    	WHERE `id` IN (".$ids.") ORDER BY FIELD(`id`, ".$ids.")";


					    //Выполняем SQL запрос
					    $r=mysql_query($sql);


					    //Выводим найденные раздачи
						for ($i=0; $i < mysql_num_rows($r); $i++) 
						{ 
							//Переводим ряд результата запроса в массив
							$f=mysql_fetch_array($r);


							//Переводим дату в русский формат
							$torrent_date=explode('-', $f[date]);
							//Можно просто развернуть массив, но для наглядности сделаем так
							$torrent_date=$torrent_date[2].'.'.$torrent_date[1].'.'.$torrent_date[0];
					?>
					<tr>
						<td width="75%"><a href="/torrent.php?id=<?=$f[id]?>"><?=$f[name]?></a></td>
						<td width="5%"><a href="magnet:?xt=urn:btih:<?=$f[hash]?>"><i class="glyphicon glyphicon-magnet"></i></a></td>
						<td width="10%"><?=bytesToSize($f[size])?></td>
						<td width="10%"><?=$torrent_date?></td>
					</tr>
					<?
						}
					?>
				</tbody>
			</table>
		</div>
		<!-- /.container -->
		<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
		<script src="http://getbootstrap.com/dist/js/bootstrap.min.js"></script>
	</body>
</html>

Для удобства пользователей, сделаем отдельную страницу для каждого торрента. Вдруг кому-то понадобится отправить ссылку.

Файл torrent.php

<?
//Фильтруем и форматируем id торрента
$id=trim(urldecode($_GET[id]));


//Если нет id, то делаем редирект на главную страницу
if (empty($id)) {header("Location: /"); exit();}


//Если id есть, то...
//Подключаемся к MySQL, при неудаче выводим ошибку
mysql_connect("localhost", "torrent", "password") or die("Could not connect to MySQL");


//Выбираем БД, при неудаче выводим ошибку
mysql_select_db("torrent") or die("Could not select database");


//Переводим все общение с БД в кодировку utf8
mysql_query("SET NAMES utf8");


//Пишем SQL запрос для выборки торрента по id
$sql="SELECT * FROM `torrents` WHERE `id`='".mysql_real_escape_string($id)."'";


//Выполняем SQL запрос
$r=mysql_query($sql);


//Если нет такого id в базе, то делаем редирект на главную страницу
if (mysql_num_rows($r)==0) {header("Location: /"); exit();}


//Переводим ряд результата в массив
$torrent=mysql_fetch_array($r);


//Переводим дату в русский формат
$torrent_date=explode('-', $torrent[date]);
$torrent_date=$torrent_date[2].'.'.$torrent_date[1].'.'.$torrent_date[0];



//Функция перевода байтов в килобайты, мегабайты и тд. Пригодится нам ниже. Описывать её прицип не имеем смысла - чистая арифметика. 
function bytesToSize($bytes, $precision = 0)
{   
        $kilobyte = 1024;
        $megabyte = $kilobyte * 1024;
        $gigabyte = $megabyte * 1024;
        $terabyte = $gigabyte * 1024;

        
        if (($bytes >= 0) && ($bytes < $kilobyte)) {return $bytes . ' B';} 
        elseif (($bytes >= $kilobyte) && ($bytes < $megabyte)) {return round($bytes / $kilobyte, $precision) . ' Kb';}
        elseif (($bytes >= $megabyte) && ($bytes < $gigabyte)) {return round($bytes / $megabyte, $precision) . ' Mb';}
        elseif (($bytes >= $gigabyte) && ($bytes < $terabyte)) {return round($bytes / $gigabyte, $precision) . ' Gb';}
        elseif ($bytes >= $terabyte) {return round($bytes / $terabyte, $precision) . ' Tb';} 
        else {return $bytes . ' B';}
    }    


?>
<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="utf-8">
		<meta http-equiv="X-UA-Compatible" content="IE=edge">
		<meta name="viewport" content="width=device-width, initial-scale=1">
		<link rel="icon" href="/favicon.ico">
		<title><?=htmlspecialchars($torrent[name])?></title>
		<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
		<style type="text/css">
			body
			{
			padding-top: 80px;
			padding-bottom: 20px;
			}
		</style>
	</head>
	<body>
		<nav class="navbar navbar-default navbar-fixed-top">
			<div class="container">
				<div class="navbar-header">
					<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
					<span class="sr-only">Навигация</span>
					<span class="icon-bar"></span>
					<span class="icon-bar"></span>
					<span class="icon-bar"></span>
					</button>
					<a class="navbar-brand" href="/">Зеркало раздач RuTracker</a>
				</div>
				<div id="navbar" class="navbar-collapse collapse">
					<form action="/search.php" method="GET" class="navbar-form navbar-left">
						<div class="form-group input-group">
							<input type="text" placeholder="" value="" class="form-control" name="q"> 
							<span class="input-group-btn">
							<button class="btn btn-primary" type="submit"><i class="glyphicon glyphicon-search"></i></button>
							</span>  
						</div>
					</form>
				</div>
				<!--/.navbar-collapse -->
			</div>
		</nav>
		<div class="container">
			<h1><?=htmlspecialchars($torrent[name])?></h1>
			<table class="table table-striped">
				<tbody>
					<tr>
						<th width="20%">Скачать:</th>
						<td><a href="magnet:?xt=urn:btih:<?=$torrent[hash]?>"><i class="glyphicon glyphicon-magnet"></i> Magnet</a></td>
					</tr>				
					<tr>
						<th width="20%">Размер:</th>
						<td><?=bytesToSize($torrent[size])?></td>
					</tr>
					<tr>
						<th width="20%">Дата раздачи:</th>
						<td><?=$torrent_date?></td>
					</tr>
					<tr>
						<th width="20%">Раздел:</th>
						<td><a target=_blank href="http://rutracker.org/forum/viewforum.php?f=<?=$torrent[cat_id]?>"><?=htmlspecialchars($torrent[cat_name])?></a></td>
					</tr>
					<tr>
						<th width="20%">Обсуждение:</th>
						<td><a target=_blank href="http://rutracker.org/forum/viewtopic.php?t=<?=$torrent[topic_id]?>">Топик #<?=$torrent[topic_id]?></a></td>
					</tr>															
				</tbody>
			</table>
		</div>
		<!-- /.container -->
		<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
		<script src="http://getbootstrap.com/dist/js/bootstrap.min.js"></script>
	</body>
</html>

Вот и всё. Мы получили полностью рабочий сайт с базой данных от RuTracker, с быстрым поиском и удобным интерфейсом. Я специально не стал добавлять фильтрацию поиска по категориям, сортировку, пагинацию и т.д, что бы был максимально чистый код с самым необходимым. Если будет интерес, я расскажу об этом всем в комментариях или в отдельной статье.

Всем большое спасибо за внимание. Пишите вопросы, всем отвечу.

Автор: lapopator

Источник [4]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/programmirovanie/107067

Ссылки в тексте:

[1] хостинге: https://www.reg.ru/?rlink=reflink-717

[2] здесь: http://rutracker.org/forum/viewtopic.php?t=4824458

[3] здесь: https://code.google.com/p/sphinxsearch/source/browse/trunk/api/sphinxapi.php

[4] Источник: http://habrahabr.ru/post/273777/