Я максимально постараюсь писать без «воды». Минимум лишней отвлекающей информации и разглагольствований. Максимум полезной информации и рабочего кода. Я не буду поднимать вопрос зачем кому-то собственный торрент-поисковик на базе RuTracker. И я не считаю себя гуру программирования. Мы просто сделаем этот сайт вместе. Будем использовать Apache+PHP, MySQL и Sphinx. Сразу предупрежу, что на минимальном виртуальном
База данных
Для начала нам надо взять саму базу. RuTracker каждый месяц выкладывает дамп своих торрентов здесь. Скачиваем, распаковываем и видим два десятка файлов CSV.
Нам нужны только те, в которых есть информация о торрентах – остальные удаляем. В файле «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):
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-файлы.
<?
//Ограничиваем время выполнения скрипта 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.
Sphinx
Установка Sphinx на различные системы производится разными способами. Все зависит от операционной системы и железа. Это тема заслуживает отдельной статьи. Но есть очень много отличных мануалов в Интернете. На русском языке тоже. Сейчас же мы займемся настройкой конфигурационного файла для Sphinx. Создаем в корневом каталоге сайта директорию, допустим, cache. Здесь будут хранится все файлы индекса Sphinx для нашего сайта. Загружаем в эту папку файл конфигурации (листинг приведен ниже).
# Настройка источника откуда берутся данные
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 минут.
После окончания индексирования, проверим все ли нормально прошло. Для этого выполним поиск через консоль с помощью команды (поисковая фраза пишется после указания файла конфига):
search --config /home/rutr/rutracker.online/www/cache/torrents.conf morrowind mod
Если вы увидели что-то похожее на верхний скриншот, значит индексирование прошло успешно. Если ничего не найдено, то не нужно запускать следующую команду. Для запуска демона поиска Sphinx выполняем команду:
searchd --config /home/rutr/rutracker.online/www/cache/torrents.conf
Обратите внимание, что демон нужно запускать после каждой перезагрузки системы. Для выключения демона (если нужно) добавляем «--stop» в конце вышеуказанной команды.
Web
Я не долго думал какой фреймворк использовать для web-интерфейса. Требования простые: простота в использовании, адаптивный дизайн и поддержка всех современных браузеров. Под это отлично подходит, пусть и немного надоевший, Bootstrap. Дистрибутив качать не обязательно, можно подключить файл стилей онлайн. Главная страница на чистом HTML, без использования 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>
Дизайн главной страницы получился очень минималистичным и максимально функциональным.
Скрипт поиска будет уже интереснее. Для начала нам нужен API Sphinx на PHP. Последнюю версию можно взять здесь. Кратко расскажу, как работает скрипт поиска, а подробнее уже в листинге. Подключаем файл для работы с API, настраиваем поиск, ищем, выкладываем результаты поиска в удобном виде. Скачать торрент можно будет прямо из поиска, без дополнительных кликов.
<?
//Фильтруем и форматируем запрос
$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>
Для удобства пользователей, сделаем отдельную страницу для каждого торрента. Вдруг кому-то понадобится отправить ссылку.
<?
//Фильтруем и форматируем 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