Pull to refresh

Разбираемся с rtorrent всерьёз

Reading time 14 min
Views 75K
Об установке и базовой настройке rtorrent на хабре хватает статей, как и споров о том, стоит ли вообще связываться с хардкорным rtorrent или лучше обойтись чем-нибудь более дружественным к пользователю. Лично я много лет назад пересмотрел все качалки и в результате rtorrent оказался самым стабильным и эффективным. Интерфейс у него не самый удобный, но достаточно понятный и юзабельный чтобы это не стало серьёзной проблемой. Альтернативные интерфейсы вроде rutorrent у меня как-то не прижились - ставить php только ради rutorrent неохота, а остальные варианты выглядят совсем слабо (и ни одного кроме rutorrent даже нет в портаж Gentoo).

  

Одно из основных преимуществ rtorrent — очень гибкие возможности по его настройке и автоматизации. К сожалению, синтаксис ~/.rtorrent.rc достаточно нестандартный, нормальная документация отсутствует, поэтому обычно настройка сводится к поиску и копированию (попытка что-то в них изменить кроме констант/путей к каталогам обычно проваливается) готовых рецептов или вообще ограничивается редактированием констант в базовой конфигурации.

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



Обновление


Настраивался rtorrent-0.9.2 (с дополнительным color-патчем, как на скриншоте выше), и если у вас более старая версия настоятельно рекомендую обновиться. После обновления стоит запустить migrate_rtorrent_rc.sh для конвертации устаревшего синтаксиса внутри ~/.rtorrent.rc в более современный и просмотреть инструкции по миграции.

Другая документация


Изучение существующей документации вызывает только одно чувство: глубокую печаль. Вру. Оно вызывает желание грубо матерится. А глубокая печаль — это то, что приходит много позже, когда понимаешь что тебе не померещилось, и всё действительно настолько плохо. Тем не менее, хоть что-то это всё-таки лучше, чем совсем ничего (вот! я сумел таки найти что-то положительное), поэтому давайте посмотрим, что у нас есть:


Отладка


Для отладки и экспериментов можно вместо изменения конфиг-файла и перезапуска rtorrent воспользоваться встроенной в него командной строкой. Вызывается она нажатием Ctrl-X, после чего можно ввести любую команду, например:

command> print="Hello ",World!

нажать Enter и получить на экране и в логе:

(22:13:39) Hello World!

Недоступные на экране старые записи из лога можно увидеть нажав l, выйти из экрана просмотра лога нажав пробел.

При использовании в командной строке команд, которые должны применяться к конкретному объекту (торренту, файлу, трекеру, пирам) — будет использован текущий выбранный в интерфейсе rtorrent объект (т.е. нужно курсором стать на нужную строку перед выполнением команды).

Основное неудобство этой командной строки — отсутствие истории, нельзя нажав «вверх» увидеть предыдущую команду, модифицировать её и снова выполнить.

Синтаксис конфиг-файла


Первое, что необходимо понять про синтаксис ~/.rtorrent.rcэто не конфиг файл. Да, выглядит похоже, но это всего лишь опасная иллюзия. На самом деле это программа: последовательность команд (это официальный термин, но я их буду дальше называть функциями, так привычнее и по сути вернее), причём активно использующая передачу одних команд (вместе с их собственными параметрами) параметрами в другие (привет, callback-и в конфигах — только вас и не хватало для полного счастья!), с несколькими уровнями вложенности и разными вариантами экранирования. Можно (и придётся) создавать свои собственные функции — это сильно помогает уменьшить количество уровней вложенности и вызванные этим сложности с экранированием.

Таким образом, когда вы видите в конфиге строку вроде:

throttle.global_down.max_rate.set_kb = 10000

то это на самом деле вызов функции с одним параметром, более традиционно записываемый как:

throttle.global_down.max_rate.set_kb(10000)

Насколько я понял, точки в именах функций не несут особого смысла, просто обычный разделитель для читабельности, как подчёркивание.

Пробелы, как ни странно, допустимы далеко не везде где используются какие-либо разделители. Как минимум, их можно использовать:

  • вокруг = после имени первой команды в строке (не важно, это строка  конфиг-файла или строка в кавычках "" посередине строки конфиг-файла)
  • после , разделяющей параметры (перед запятой по-моему работает не  везде)
  • вокруг ; разделяющей команды в строке, описывающей тело новой функции

Вызов функции

# без параметров, аналог func()
func=
# с двумя параметрами, аналог func("param1","param2")
func=param1,param2

Значение параметра трактуется как строка. Брать значение параметра в кавычки необходимо если он содержит пробел или символ-разделитель вроде , или ; (использовать кавычки всегда — для наглядности - затруднительно, т.к. очень часто этот параметр и так уже находится внутри строки содержащей несколько команд, так что придётся использовать экранирование кавычек и наглядность в результате только ухудшится).

func="first param, with special chars","second;param"

Чтобы получить результат вызова функции (для передачи его параметром в другую функцию) нужно перед именем функции поставить $:

# выведет: system.hostname=
print=system.hostname=
# выведет: system.hostname=
print="system.hostname="
# выведет: powerman.name
print=$system.hostname=
# выведет: powerman.name
print="$system.hostname="

В первом и втором примерах функция print получает один параметр: строку "system.hostname=". В третьем и четвёртом print получает параметром результат выполнения функции system.hostname без параметров: строку "powerman.name".

Функция print выводит все свои параметры, и если одним из них будет вызов функции, у которой тоже есть параметры — необходимо разделить, какие параметры относятся к print, а какие к другой функции (на самом деле это не очень актуальная проблема, т.к. большинство функций rtorrent параметры или вообще не принимают или принимают один):

# выведет: oneСб сен 27 23:50:51 EEST 2014-utwo
print=one,$execute.capture=date,-u,two
# выведет: oneСб сен 27 20:51:05 UTC 2014two
print=one,"$execute.capture=date,-u",two

Функция execute.capture выполняет любую системную команду (через sh) и возвращает то, что эта команда выведет на STDOUT. Поэтому она может принимать любое количество параметров (имя команды и параметры для неё). Как видите в первом варианте она взяла только один параметр (и вызвала date) а остальные (-u,two) достались print-у. А во втором мы явно указали какие параметры относятся к execute.capture, поэтому она вызвала date -u а print-у достался только последний параметр (two).

Если вы попытаетесь запустить этот пример — он у вас не сработает как описано. Дело в том, что системная команда date завершает вывод переводом строки, так что остальные параметры print-а выводятся уже на другой строке и rtorrent это показывает не очень внятно, даже в логе. Поэтому, чтобы пример сработал, нужно подменить команду date вариантом, который перевод строки не выводит, например положить в ~/bin/date:

#!/bin/bash
echo -n `/bin/date "$@"`

callback-и

Некоторые функции rtorrent ожидают в своих параметрах переданные пользователем функции (которые они будут вызывать сами позднее). Есть два альтернативных синтаксиса, как можно передать одну функцию параметром в другую: либо передаётся обычная строка содержащая вызов функции и её параметры в обычном синтаксисе rtorrent, либо передаётся список строк внутри (( )), где имя функции это первая строка а остальные строки её параметры.

# func() вызывается с двумя параметрами:
# - пользовательским callback-ом func1()
# - строкой "param2"
func=func1=,param2
# func() вызывается с двумя параметрами:
# - пользовательским callback-ом func1("param11","param12")
# - строкой "param2"
func="func1=param11,param12",param2
# то же самое, что и в предыдущем примере
func=((func1,param11,param12)),param2

Соответственно, если передаваемому callback-у func1 в параметрах нужно тоже передать callback, то нужно либо использовать разные стили экранирования (кавычки/скобки), либо экранировать слешем кавычки внутри кавычек, либо использовать вложенные скобки.

# передадим в func1() первым параметром вместо строки param11
# callback func2("param21","param22") разными способами:
func="func1=\"func2=param21,param22\",param12",param2
func="func1=((func2,param21,param22)),param12",param2
func=((func1,((func2,param21,param22)),param12)),param2
func=((func1,"func2=param21,param22",param12)),param2

Кстати, в rtorrent есть ещё один способ синтаксически передать список - внутри { }. Где и для чего его стоит применять я пока не разбирался.

Пользовательские функции

Новые функции можно создавать используя функцию method.insert. Первым параметром передаётся имя для новой функции, вторым её тип (обычно simple, но если нужно создать не функцию а переменную то нужно использовать типы value, bool, string; так же можно сделать функцию/переменную private и const и не только — к сожалению, разбираться с этими возможностями нужно по исходникам), третьим строка с телом функции. В отличие от строк с callback-ами, описанными выше, в этой строке можно передать несколько команд через ; но нельзя использовать разделитель (( )).

# создаём новую функцию:
method.insert = newfunc, simple, "func=param1,param2;func1=;func2="
# и вызываем её:
newfunc=

Если нужно вызывать созданную функцию с разными параметрами (до 4-х) — к ним можно обратиться внутри функции через argument.0=argument.3.

method.insert = hello, simple, "print = \"Hello \", $argument.0=, !"
# выведет: Hello Powerman!
hello=Powerman

Переменные


С переменными работа идёт по сути тоже через функции (get/set), причём обычно есть несколько альтернативных вариантов записи.

# установить some.var в значение "value"
some.var.set = value
some.set_var = value
some.var = value
# получить значение some.var
$some.var=
$some.var.get=
$some.get_var=

Насколько я понял, первый из показанных выше вариантов считается современным и должен работать для любых переменных, а все остальные использовались в старых версиях rtorrent и сейчас поддерживаются для некоторых переменных ради обратной совместимости.

Чтобы не заморачиваться с method.insert ради создания пары переменных есть готовые функции позволяющие работать с переменными привязанными к конкретному торренту (что позволяет сохранять информацию по каждому торренту отдельно):

# установить переменную var в значение value для текущего торрента
d.custom.set = var, value
# получить значение переменной var для текущего торрента
$d.custom=var

# установить 5 предопределённых переменных для текущего торрента
d.custom1.set = value1
d.custom2.set = value2
d.custom3.set = value3
d.custom4.set = value4
d.custom5.set = value5
# получить 5 предопределённых переменных для текущего торрента
$d.custom1=
$d.custom2=
$d.custom3=
$d.custom4=
$d.custom5=

Выше я писал, что точки в именах функций не несут особого смысла и по сути не отличаются от подчёркиваний. Как видите, это не совсем так: переменные начинающиеся на d. связаны с конкретным торрентом, и у каждого торрента свои значения этих переменных. Аналогично с функциями начинающимися на d. — они тоже оперируют текущим торрентом. Тем не менее, я думаю что это особенность реализации встроенных функций, а для пользовательских функций и переменных наличие или отсутствие d. в имени не сделает их магически связанными с текущим торрентом (впрочем, я это не проверял).

Странности и баги


Если callback для schedule передаётся внутри (( )) а не " ", то параметры для этого callback так же заданные внутри вложенных (( )) почему-то обрабатываются как получение результата функции, а не передача её как callback:

schedule id,1,1, ((print, ((directory.default))))
# работает как
schedule id,1,1, "print = $directory.default="

Некоторые функции глючат, если вызываются при запуске rtorrent (например load.start или ui.current_view.set) — их выполнение нужно отложить через schedule, причём start time должен быть больше 0. Вероятно это вызвано тем, что при запуске rtorrent, к моменту когда выполняются команды из конфиг-файла, ещё не все структуры данных корректно инициализированы.

Краткий обзор полезных функций


print = список строк

Отладка в командной строке; вывод в лог.

$cat = список строк

Возвращает сконкатенированные строки (аналог print, только возвращает строку вместо вывода в лог).

$argument.0=
$argument.1=
$argument.2=
$argument.3=

Получить 1/2/3/4-й параметр текущей функции.

method.insert = имя_функции, simple, "тело; функции"

Создать пользовательскую функцию.

По-моему только в method.insert и method.set_key можно передать последовательность из нескольких команд через ; — все остальные функции куда можно передавать команды принимают их либо по одной, либо как список (напр. load.normal и load.start).

method.set_key = event.download.тип, имя_события, "список; команд"

Создать обработчик события. Допустимые значения для тип: insertedinserted_new, inserted_session, erased, opened, closedresumed, paused, finished, hash_done, hash_failedhash_final_failed, hash_removed, hash_queued. Зачем нужно имя_события не понятно (вероятно, его можно проверить через method.has_key и method.list_keys но неясно где это может пригодиться), но легко догадаться что оно должно быть уникальным.

schedule = имя_расписания, начало, интервал, список команд
schedule_remove = имя_расписания

Периодически выполняемый список команд, основной инструмент для автоматизации rtorrent. Значения для начало и интервал можно задавать либо в секундах либо в формате чч:мм:сс (если интервал установлен в 0 выполнится однократно). Для команд которые должны выполниться при запуске rtorrent значение для начало обычно устанавливают в 5 — вероятно, чтобы rtorrent успел полноценно запуститься и не возникали упомянутый мной выше баги.

load.normal = путь_к_torrent_файлу(ам), список команд
load.start  = путь_к_torrent_файлу(ам), список команд

Обычно вызывается из schedule для автоматической загрузки файлов добавленных в заданный каталог. load.start автоматически запускает выкачку файла после добавления.

execute             = системная_команда, список её параметров
execute.capture     = системная_команда, список её параметров
execute.nothrow     = системная_команда, список её параметров
execute.nothrow.bg  = системная_команда, список её параметров

Выполняет внешнюю программу. execute.capture возвращает вывод этой команды, execute.nothrow игнорирует ошибку запущенной команды, execute.nothrow.bg запускает программу в фоне.

if      = выражение,    команда_если_истина, команда_если_ложь
branch  = команда_тест, команда_если_истина, команда_если_ложь

Позволяет выбрать выполняемую команду в зависимости от условия. Разница между if и branch в том, как задаётся условие: как значение ($func=) или как команда (func=). Кроме того вместо команда_если_ложь можно задать следующее условие и добавить ещё два параметра-команды, и т.д.

false=
not     = команда
and     = список команд
or      = список команд
equal   = список команд
less    = список значений
greater = список значений

Думаю, общий смысл понятен. Помимо использования в if и branch функции less и greater используются для настройки разных сортировок.

У этих и последующих команд формат параметров я не проверял, так что могут быть ошибки (пишите в личку, исправлю).

import  = конфиг-файл

Подключение дополнительного конфиг-файла.

start_tied=
stop_untied=
close_untied=
remove_untied=

Tied-файлом rtorrent называет тот .torrent-файл, который вы скачали и загрузили в rtorrent (обычно — автоматически загружая все .torrent-файлы из заданный каталогов через schedule и load.start). Что любопытно, для выкачки rtorrent использует не этот файл, а его копию, которую он сохраняет в свой каталог session.path. Соответственно, stop_untiedclose_untied и remove_untied при выполнении определяют, что сделать с торрент-файлом в каталоге session.path если оригинальный торрент-файл связанный (tied) с ним был удалён. Что делает команда start_tied я пока не понял.

$d.base_filename=
$d.base_path=
$d.creation_date=
$d.directory=
$d.load_date=
$d.name=
$d.tied_to_file=

Разные свойства текущего торрента. На самом деле их в несколько раз больше, я просто упомянул самые полезные на первый взгляд. Например, d.directory это каталог куда будет выкачиваться торрент, d.tied_to_file имя оригинального (не из session.path) .torrent-файла, d.name имя файла или каталога выкачиваемого этим торрентом, etc.

$d.complete=

Текущее состояние — этот торрент скачался или ещё нет.

d.close=
d.create_link   = тип, префикс, постфикс
d.delete_link   = тип, префикс, постфикс
d.delete_tied=
d.erase=
d.open=
d.pause=
d.resume=
d.save_full_session=
d.start=
d.stop=

Разные операции над текущим торрентом. Значение тип может быть base_path, base_filename и tied и определяет каталог для симлинка (из d.base_path, d.base_filename или d.tied_to_file), префикс и постфикс добавляются к имени и могут быть пустыми.

d.multicall

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

Пример из моего конфига


При появлении .torrent-файла в одном из каталогов /mnt/torrent//mnt/torrent/serials/ и /mnt/torrent/music/ он должен быть:

  • автоматически загружен в rtorrent
  • настроен на выкачку в тот же каталог где лежит .torrent-файл
  • торренты из каталога /mnt/torrent/music/ должны сразу запуститься

schedule = watch_directory_1, 5, 5, ((load.normal, /mnt/torrent/*.torrent,         "d.custom.set = watchdir, /mnt/torrent",         "d.directory.set = $d.custom=watchdir"))
schedule = watch_directory_2, 5, 5, ((load.normal, /mnt/torrent/serials/*.torrent, "d.custom.set = watchdir, /mnt/torrent/serials", "d.directory.set = $d.custom=watchdir"))
schedule = watch_directory_3, 5, 5, ((load.start,  /mnt/torrent/music/*.torrent,   "d.custom.set = watchdir, /mnt/torrent/music",   "d.directory.set = $d.custom=watchdir"))

  • автоматически переименован чтобы соответствовать имени файла или  каталога который он будет выкачивать (т.е. файл  [rutracker.org].t4788972.torrent автоматически переименуется в  Читающий Мысли (The Listener) (Season V, 2014, WEB-DL 720p) [FOX].torrent)
  • перед расширением .torrent к имени файла должен быть добавлен суффикс  показывающий его текущий статус: --- если он ещё не скачался или +++  если он уже скачался

method.insert = d.renamed_suffix,   simple, "if = $d.complete=, +++, ---"
# - WORKAROUND: extra / at beginning needed because $d.tied_to_file= begins with //
method.insert = d.renamed_file,     simple, "cat = /, $d.custom=watchdir, /, $d.name=, ., $d.renamed_suffix=, .torrent"
method.insert = d.rename_file,      simple, "execute = mv, --, $d.tied_to_file=, $d.renamed_file=; d.tied_to_file.set = $d.renamed_file="
method.insert = d.safe_rename_file, simple, "branch = ((equal, d.tied_to_file=, d.renamed_file=)), , d.rename_file="
method.set_key = event.download.inserted_new,   rename_loaded,  d.safe_rename_file=
method.set_key = event.download.resumed,        rename_resumed, d.safe_rename_file=
method.set_key = event.download.finished,       rename_finished,d.safe_rename_file=

Если .torrent-файл удаляется то его копию из session.path тоже нужно удалить и убрать этот торрент из rtorrent.

# Watch a directory for torrents, and remove those that have been deleted.
schedule = watch_untied,      5, 5, ((remove_untied))

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

# Notify when download finished
method.set_key = event.download.finished, notify_me, "execute = ~/bin/rtorrent_finished, $d.name="

После запуска rtorrent открыть список ещё недокачанных торрентов (аналог нажатия 6 вручную).

# Set default view
schedule = default_view, 1, 0, ui.current_view.set=incomplete

Отсортировать список недокачанных по дате добавления (этот код взят из рецепта в wiki и здесь приведён в качестве примера использования less=).

method.set_key = event.download.inserted_new,   loaded_time,  "d.custom.set = tm_loaded, $system.time=; d.save_full_session="
view.sort_new       = incomplete, less=d.custom=tm_loaded
view.sort_current   = incomplete, less=d.custom=tm_loaded

Заключение


Надеюсь, это описание поможет автоматизировать работу вашего rtorrent и решит проблему отсутствия подходящих лично вам рецептов. К сожалению, качество этой статьи заметно ниже моих обычных статей, многие вопросы до конца я так и не прояснил, в описаниях некоторых функций наверняка есть ошибки, но количество времени которое было не жалко убить на эту задачу было сильно ограничено. Вероятно, стоит использовать информацию из этой статьи для дополнения официальной wiki, но у меня на это уже сил и времени нет, будем надеяться это сделает кто-нибудь другой.

______________________
Текст конвертирован используя habrahabr backend для AsciiDoc.
Tags:
Hubs:
+67
Comments 63
Comments Comments 63

Articles