четверг, 11 апреля 2013 г.

Кошерная настройка rtorrent и пояснения к конфигу .rtorrent.rc

Сегодня мы будем настраивать консольный rTorrent (файл конфига ~/.rtorrent.rc), вот некоторые цели:
 1. мониторить папку с новыми .torrent-файлами, сразу переносить их в другое место, чтоб было ясно, что файл подхватился и пошла загрузка
 2. по завершении загрузки переносить контент в общую папку-файлопомойку
 3. по достижении закачкой ratio=1.5 закрывать ее и удалять .torrent-файл
 4. при закрытии закачки дергать bash-скриптик, передавая ему путь к торренту, дабы тот мог сделать с ним что угодно
 5. ограничивать скорость отдачи и скачивания по расписанию
 6. если скачан фильм - переносить его в отдельную папку и называть нормально и по-русски, например "Дом с паранормальными явлениями (2013).avi" вместо Dom.s.paranormalnymy.yavleniyamy.2013.Dc.DVDRip.avi

Итак, поехали

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

Начнем со стандартных настроек:

port_range = 6851-6899
download_rate = 2000 # скорость скачивания 2Мбайт/с
upload_rate = 1000  # скорость отдачи, 1000кбайт/с
session = /RTORRENT/session
directory = /RTORRENT/TMP # тут контент находится, пока скачивается
check_hash = yes
scgi_port = 0.0.0.0:5000 # это для wtorrent+nginx

много позже я добавил еще такие
dht = auto
dht_port = 6850
peer_exchange = yes
теперь по целям..

Цель 1

schedule = watch_directory, 5, 5, "load_start=/RTORRENT/newtorrents/*.torrent, d.set_custom1=/RTORRENT/DONE/"
Эта команда каждые 5 секунд проверяет папку /RTORRENT/newtorrents/, открытую на полный доступ, на наличие новых .torrent файлов. d.set_custom1 это команда, которая сохраняет для каждого торрента его итоговую папку назначения (куда он будет перенесен после закачки), подробнее будет в пункте "Цель 2". Также удобно получается мониторить несколько папок. 
Например можно написать такие конфиг-строки отдельно для newtorrents/films, newtorrents/games, и т.д. и для каждой прописать свою итоговую папку.
Хрен знает, почему 5 секунд указано 2 раза, зато вместо watch_directory можно написать что угодно, этот идентификатор придумывается самостоятельно.

Вообще доступные команды имеют вид "d.<command>=", т.е. всегда начинается с "d." и всегда заканчивается на "=", даже если ничего не присваивается (например команда "d.stop=").
Есть класс команд для получения каких-то значений, они отличаются тем, что начинаются с "$d.", например "$d.get_name=". Заканчиваются также знаком "=".  $ похоже означает, что вызов должен вернуть значение, а не просто выполнить какое то действие.
Полный список команд можно получить, отправив xmlrpc запрос "system.listMethods" на хост, где крутится wtorrent, если такой есть (xmlrpc wtorrent.local/RPC2 system.listMethods), либо посмотреть тут https://mdevaev.github.io/emonoda/rTorrent-XMLRPC-Reference/

Как видно из списка, есть 6 custom-полей, которые можно установить, используя в своих нуждах:
Index 42 String: 'd.custom1'
Index 43 String: 'd.custom1.set'

Т.е. в команде d.set_custom1 выше имя не от балды взято.
Если имеется вариант с ".set", значит поле можно установить, в этом случае команда записывается как "d.set_tied_to_file=/filename", если нет, значит поле только для чтения, тогда использовать его можно только так "$d.get_tied_to_file=".

Для достижения цели 1 осталось при загрузке нового торрента перенести его .torrent-файл в какую-нить другую папку, чтоб он исчез из общего доступа и стало ясно, что он подхватился.
Используем такую команду
system.method.set_key = event.download.inserted_new, move_loaded, "execute=mv,-u,$d.get_loaded_file=,$d.get_meta_path=;d.set_tied_to_file=$d.get_meta_path="
Вот в душе не понимаю, откуда берутся эти магические заклинания (подчеркнуты), move_loaded - опять же идентификатор, придумывается от балды, дальше идет команда: execute - выполняется shell-команда, параметры перечисляются через запятую. Команда перемещает .torrent-файл (путь к нему возвращает вызов $d.get_loaded_file=) в новое место - $d.get_meta_path= (это кастомный метод, возвращает полный путь к .torrent-файлу в новой папке, создан вручную, об этом ниже).Точка с запятой означает окончание текущей команды, как и в unix-shell. Можно написать много команд подряд.
В конце концов обновляем инфу для торрента: d.set_tied_to_file=$d.get_meta_path=, этим установили в tied_to_file новый путь к .torrent-файлу.

Новый метод создается следующим образом:
system.method.insert = d.get_loaded_basename, string|simple, "execute_capture=basename,$d.get_loaded_file="
system.method.insert = d.get_meta_path, simple, "cat=/RTORRENT/torrent/,$d.get_loaded_basename="
Первый метод возвращает basename торрент-файла, второй присовокупляет его к новой папке, в которой временно хранятся эти торрент-файлы.
Имена d.get_loaded_basename и d.get_meta_path придумываются от балды, но с оглядкой на существующие методы. "d." в них для единообразия с остальными. Переопределять существующие не пробовал, но думаю ничего полезного не выйдет.

Цель 2

Чтобы перенести загруженный контент в папку для готовых, воспользуемся событием event.download.hash_done. Но сначала заведем пару вспомогательных методов:
system.method.insert = movecheck1, simple, "and={d.get_complete=,d.get_custom1=}"
system.method.insert = movedir1, simple, "d.set_directory=$d.get_custom1=;execute=mv,-u,$d.get_base_path=,$d.get_custom1=;d.set_custom1=;d.stop=;d.start="
Первый проверяет готов ли действительно торрент и есть ли у него папка назначения.
Второй собственно устанавливает закачке папку из поля custom1, переносит контент в эту папку, стирает поле custom1 (это важно для другой фичи), и перезапускает (stop/start), чтобы обновилась вся инфа (вообще хз, не помню зачем). 
Ну а запускается все это великолепие по событию:
system.method.set_key = event.download.hash_done, move_hashed1, "branch={$movecheck1=,movedir1=}"
Не совсем понимаю, какую роль тут играют {скобки}, работает и без них, но стоит обратить внимание, что movecheck1 записано с "$", ибо это get-вызов, возвращает boolean, а movedir1= - без "$", ибо это вызов команды.

Сюда же запишем еще одну фичу - удаление недокачанного контента:
system.method.set_key = event.download.erased, rm_files, "branch=d.get_custom1=,\"execute={mv,$d.get_base_path=,/RTORRENT/RM/}\""
По событию удаления закачки (двойное Ctrl+D в консоли клиента), проверяется, заполнено ли поле custom1, если заполнено - значит торрент недокачан и выполняется команда mv to /RM (для отладки, можно и сразу удалять командой rm -r). Если торрент докачан, как мы помним, у него стирается поле custom1, а значит удаление закачанного торрента из списка в клиенте не приводит к удалению файлов.

Цель 3 и 4

Для закрытия закачки определяются условия достижения ratio (min и max - в процентах):
ratio.enable=
ratio.min.set=150
ratio.max.set=300
ratio.upload.set=20M
system.method.set = group.seeding.ratio.command, "execute=~/scripts/rt_finished,$d.get_base_path=,$d.get_tied_to_file=", d.close=, d.erase=
Тут почему то уже method.set, вместо method.set_key. Магия.
По достижению ratio 1.5, вызывается custom-скрипт, ему передается путь к контенту и .torrent-файлу, когда скрипт отрабатывает - вызывается d.close= и d.erase=, закачка полностью закрывается, исчезает из клиента, удаляется .torrent-файл, остается только контент.

Важно отметить, что код выхода скрипта влияет на поведение команды, если выйти с "exit 1", то клиент отобразит в консоли ошибочное сообщение и закачку не удалит.
Иногда хочется удалить (закрыть) готовую закачку из списка вручную, но чтобы скрипт отработал так, как если бы был достингут ratio 1.5. Для этого добавляем такую строку
system.method.set_key = event.download.erased, mv_film, "branch=$d.get_complete=,\"execute={~/scripts/rt_finished,$d.get_base_path=,$d.get_tied_to_file=}\""
Т.е. по событию erased теперь отрабатывает 2 участка конфига: rm_files и mv_film. Но у них разные условия: первый стирает недокачанные закачки на основании поля get_custom1, второй передает закачку скрипту на обработку на основании флага get_complete

Цель 5

schedule = throttle_1,18:00:00,24:00:00,download_rate=1500
schedule = throttle_2,00:00:00,18:00:00,download_rate=3000
Ночью и днем, когда все на работе - жарить по полной, 3Мб/с, вечером - притормаживать.

Цель 6

Реализация этой цели полностью в custom-скрипте (см.также UPD ниже).
Для того, чтобы фильмы переносились в отдельную папку, при постановке новой закачки надо не полениться и назвать .torrent-файл полным русским именем фильма. Если скрипт не найдет русских букв - он не примет решение о сортировке и завершится.
При закрытии закачки, rtorrent дернет скрипт, передав ему путь к контенту и имя .torrent-файла, в имени скачанного кина обычно присутствует год, он будет вычленен, добавлен к русскому имени .torrent файла, таким образом получится имя типа "Дом с паранормальными явлениями (2013).avi", с таким именем фильм и попадет в папку для сортировки.

Тут нужно отметить, что rtorrent будет ждать завершения скрипта, и если фильм переносится на другую ФС, клиент подвиснет, пока файл не переместится.

Скрипт тут:
#!/bin/sh
if [ "$1" = "" ] || [ "$2" = "" ] ; then
    echo "Usage: $0 <path_to_film> <path_to_file.torrent>"
    exit 1
fi
DIRNAME=`dirname $0`
SORTING_LOCATION="/FILMS/НА СОРТИРОВКУ"
LOGNAME=$DIRNAME/rt_finished.log
# имя .torrent-файла без расширения .torrent
LEGAL_NAME=`basename "$2" | sed 's/\.torrent$//'`
# есть русские буквы, значит торрент назван вручную
ACCEPTED_FOR_MOVE=`echo "$LEGAL_NAME" | grep -oE "[а-яА-Я]+" | wc -l`
# год ищем в оригинальном имени файла/папки
YEAR=`echo "$1" | grep -oEi "(\b(2[0-1][0-9]{2}|19[0-9]{2})\b)" `
EXTENSION=`echo "$1" | grep -oEi "\.([a-z0-9]{3})\s*$" | grep -oEi "([a-z0-9]{3})"`
EXTLIST="avi|mkv"
if [ ! -e "$1" ] ; then
    echo file $1 not exists >> $LOGNAME
    exit 1
fi;
if [ -f "$1" ] ; then
    IS_FILM=`echo "|$EXTLIST|" | grep "|$EXTENSION|" | wc -l`
else
    IS_FILM=`ls "$1" | grep -E "\.($EXTLIST)" | wc -l`
fi
if [ "$YEAR" != "" ] ; then
    LEGAL_NAME="$LEGAL_NAME ($YEAR)"
fi
# перемещаем пока только фильмы
if [ $IS_FILM = 0 ] ; then
    echo "$1 not a film" >> $LOGNAME
    exit 1
fi
# тут доп.условия по принятию решения о перемещении: размер, новое имя
# не перемещаем кино, если в названии торрент-файла нет русских букв!
if [ $ACCEPTED_FOR_MOVE = 0 ] ; then
    echo "$LEGAL_NAME not accepted for move" >> $LOGNAME
    exit 0 # выходить лучше со статусом 0, иначе торрент принимает это за ошибку
fi
NEW_LOCATION="$SORTING_LOCATION/$LEGAL_NAME"
if [ "$EXTENSION" != "" ] ; then
    NEW_LOCATION="$NEW_LOCATION.$EXTENSION"
fi
echo "Move $1 to \"$NEW_LOCATION\" ($IS_FILM, $EXTENSION, $YEAR)" >> $LOGNAME
# перемещаем фильм в папку для сортировки
mv "$1" "$NEW_LOCATION"

Небольшой бонус

для отладки событий и команд конфига удобно использовать сам консольный клиент, в нем можно выполнять команды ручками, достаточно выбрать стрелками какую-то из закачек в списке и нажать Ctrl+X, откроется приглашение на ввод команды.
command>

можно вводить, например, 
print=$d.get_loaded_file=
d.stop=
print="$execute_capture=basename,$d.get_loaded_file="
Еще финт: иногда ставишь пачку файлов качаться, и надо чтобы один из них (фильм например, который сейчас смотреть будешь) скачался максимально быстро. Тогда ставишь все остальное на паузу (Ctrl+K), и начинаешь спокойно смотреть (особенно если торрент качает последовательно). Но потом лениво опять лезть запускать все остальные торренты, или вообще забыть можно.
Помогает такая конфиг-строка
system.method.set_key = event.download.finished,unpause_next,"d.multicall=,d.check_start="
Она по окончании закачки, снимает с паузы все остальные.
Хотел разобраться как сделать, чтоб снималось по одному файлу за раз, но не осилил.

И еще: иногда дома свет выключают и сервак жестко вырубается вместе с виртуалками (ИБП бывает не хватает, а обратной связи с серваком у него нет, чтоб погасить), когда свет дают - сервак заводится, запускает виртуалки, но вот торрент-клиент после таких аварий мне приходилось запускать руками. Теперь же дошли руки и я прописал в /etc/rc.local такую строку:
sudo -u myusername screen -UdmS rtorrent /bin/bash /home/myusername/scripts/run_rtorrent &
Эта строка запускает под моим юзером screen-сессию в режиме демона. а в ней - сам rtorrent. И когда я захожу в консоль, я могу открыть этот screen с уже работающим клиентом.

UPD. Обновление по пункту Цель 6.
Во-первых, каждый раз называть .torrent правильным именем фильма неудобно, во-вторых, можно и ошибиться в названии, в-третьих, любой руссконазванный торрент типа считается фильмом, это все же неверно.
И еще, надо отучить скрипт переноса вешать торрент клиент и веб-интерфейс на время переноса на другую ФС.

Поэтому в дело была введена более тяжелая артиллерия, нежели баш-скриптинг: PHP+либа BEncode/BDecode.
Был написан более сложный php-скрипт вместо первоначального баш-скриптика.
Его функции:
 - раскодирование метаинформации из .torrent-файла
 - получение списка файлов торрента и определение по нему, похожа ли закачка на кино
 - получение URL из поля comment, рутрекер и ннм-клуб хранят там ссылки на посты
 - получение HTML поля <title> по указанному урлу, выделение из него названия фильма

Также из метаинфо торрента определяется год выпуска фильма. В итоге без лишних телодвижений имеем автоматический поиск русского названия фильма и года, что и используется для формирования нормального названия фильма при переносе в папку назначения.

Если скрипт определил все, что требуется для переноса (иначе он плюется разными exit-кодами), то переходим к действию: форкаем процесс, parent возвращает 0 (типа все ок), child спокойно переносит фильм на другую ФС.
Это предотвращает подвисания торрент-клиента; было особенно неприятно при переносе докачанных HD фильмов.

Ссылки
http://pear.php.net/package/File_Bittorrent2/docs/latest/File_Bittorrent2/File_Bittorrent2_Decode.html
http://habrahabr.ru/post/119753/

UPD2:
некоторые команды, выполняемые по Ctrl+X
command> ui.current_view.set=stopped # переключение на вид stopped
command> d.multicall=,d.start= # запуск всех остановленных закачек
command> d.multicall=stopped,d.start= # запуск всех остановленных закачек в окне stopped

UPD3:
столкнулся с проблемой периодических вылетаний торрент-клиента. Из-за чего конкретно он падал - не выяснил, но методом тыка подобрал верную конфиг-строчку для завершения закачки (проблема в методе movedir1):
system.method.insert=movedir1,simple,"d.stop=;execute=mv,-u,$d.get_base_path=,$d.get_custom1=;d.set_directory=$d.get_custom1=;d.set_custom1=;execute=sleep,1;d.start="
UPD4:
где-то через полгода на хабре появилось очень крутое описание синтаксиса файла rtorrent.rc
http://habrahabr.ru/post/238413/


6 комментариев:

  1. это самый сумасшедший мануал! времени нет и желания так наворачивать свой клиент, не так часто пользуюсь, но позже — обязательно так и сделаю. в закладки

    ОтветитьУдалить
  2. Глупый вопрос... а не проще для завершения закачки использовать событие event.download.finished?
    event.download.hash_done вызывается так же при перезапуске торрента для всех завршенных закачек.

    ОтветитьУдалить
    Ответы
    1. >> при перезапуске торрента для всех завршенных закачек
      наверное все же "незавершенных"

      а on finished дергается когда закачку временно закрываешь через Ctrl+K.так что тоже ничем не лучше.
      on hash_done хоть и вызывается иногда не в тему, но там проверяется условие завершенности закачки и в итоге все работает как надо.

      Удалить
    2. хотя не, похоже насчет Ctrl+K и finished я неправ. это был какой то глюк.
      в общем стабильной работы удалось добиться с hash_done

      Удалить
    3. Проверил, да, был не прав. При включении торрента он вызывается для всех закачек. Но у вас они фильтруются проверкой movecheck1: завершенные отбрасываются по d.get_custom1=, закачки - по d.get_complete=

      Удалить
  3. Интересно, автор сюда посмотрит спустя 6 лет?
    При настройке по мануалу возникла проблема: хотел что бы rtorrent искал *.torrent файлы в нескольких директориях и качал торрент в ту же директорию, в которой он его нашёл.
    Например:

    schedule = watch_directory_3,5,5,"load_start=/media/user/Shara/TV_series/*.torrent, d.set_custom3=/media/user/Shara/TV_series/"

    Он находит, но упорно качает в директорию указанную тут:

    directory = /media/karamba/Shara/Torrents/

    Помогите, расскажите что делаю не так?

    ОтветитьУдалить