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

в 7:01, , рубрики: Apache, php, sphinx, sql, Большая база данных, Веб-разработка, Программирование

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

image

База данных

Для начала нам надо взять саму базу. RuTracker каждый месяц выкладывает дамп своих торрентов здесь. Скачиваем, распаковываем и видим два десятка файлов 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. Последнюю версию можно взять здесь. Кратко расскажу, как работает скрипт поиска, а подробнее уже в листинге. Подключаем файл для работы с 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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js