Common Configuration Use-Cases

After you went through Configuration Quick Start and got familiar with the handling of rTorrent, it’s time to look at settings that you should consider for your configuration, but which weren’t necessary to start using it.

The Common Tasks in rTorrent wiki page contains more of these typical configuration use-cases.

Load ‘Drop-In’ Config Fragments

The examples here and in the wiki are mostly short snippets written to serve a specific purpose. To easily add those by just dropping them into a new file, add this to your main configuration file (which then can be the last change you apply to it).

method.insert = cfg.drop_in, private|const|string, (cat, (cfg.basedir), "config.d")
execute.nothrow = bash, -c, (cat,\
    "find ", (cfg.drop_in), " -name '*.rc' ",\
    "| sort | sed -re 's/^/import=/' >", (cfg.drop_in), "/.import")
try_import = (cat, (cfg.drop_in), "/.import")

To test the change, execute these commands:

mkdir -p ~/rtorrent/config.d
echo 'print="Hello from config.d!"' >$_/hello.rc

Then restart rTorrent, and you should see Hello from config.d! amongst the initial console messages.

Note

If a drop-in file just contains commands that can be repeated several times, they can be re-imported making them way easier to test after changes. For example, schedules can be redefined, but method definitions can not (under the same name).

Log Rotation, Archival, and Pruning

The following longer snippet adds logs that don’t endlessly grow, get archived after some days, and are finally deleted after a while. See rtorrent.d/15-logging.rc for the full snippet.

Warning

If you include this, take care to comment out any conflicting logging commands that you already have in your main configuration.

The time spans for archival and pruning are set using pyro.log_archival.days (default: 2) and pyro.log_retention.days (default: 7). You can change these in your main configuration, after including the snippet via import.

# Note that the "main" log is only rotated when using rTorrent-PS 1.1+ (after 2017-03-26),
# since 'log.open_file' needed to learn how to re-open first. Otherwise, you'll get a daily
# console warning.

# Settings for archival delay, and retention [days]
method.insert.value = pyro.log_retention.days, 7
method.insert.value = pyro.log_archival.days,  2

Log files are time stamped (see pyro.date_iso.log_stamp and pyro.log_stamp.current). Full log file paths for different types are created using pyro.logfile_path, which takes the type as an argument.

# Create a "YYYY-mm-dd-HHMMSS" time stamp
method.insert = pyro.date_iso.log_stamp, simple|private,\
    "execute.capture_nothrow = bash, -c, \"echo -n $(date +%Y-%m-%d-%H%M%S)\""

# String value for the currently used time stamp, changed on rotation
method.insert = pyro.log_stamp.current, string

# Create a full logfile path using the current stamp
method.insert = pyro.logfile_path, simple|private,\
    "cat = (cfg.logs), (argument.0), \"-\", (pyro.log_stamp.current), .log"

The pyro.log_rotate multi-method takes care of calculating a new time stamp, and rotating all the log files by re-opening them with their new name. A daily schedule calls this method and thus triggers the rotation.

# (Re-)open all logs with a current time stamp; the main log file
# is just opened, you need to add some logging scopes yourself!
method.insert = pyro.log_rotate, multi|rlookup|static
method.set_key = pyro.log_rotate, !stamp,\
    "pyro.log_stamp.current.set = (cat, (pyro.date_iso.log_stamp))"
method.set_key = pyro.log_rotate, execute,\
    "log.execute = (pyro.logfile_path, execute)"
method.set_key = pyro.log_rotate, messages,\
    "branch = (pyro.extended), ((log.messages, (pyro.logfile_path, messages) ))"
method.set_key = pyro.log_rotate, xmlrpc,\
    "log.xmlrpc = (pyro.logfile_path, xmlrpc)"
method.set_key = pyro.log_rotate, ~main,\
    "log.open_file = log, (pyro.logfile_path, rtorrent)"

# Logrotate schedule (rotating shortly after 1AM, so DST shenanigans
# are taken care of, and rotation is always near the next day's begin)
schedule2 = pyro_daily_log_rotate, 01:05:00, 24:00:00, ((pyro.log_rotate))

Finally, two schedules take care of daily archival (1:10 AM) and pruning (1:20 AM), passing the command built by pyro._logfile_find_cmd to bash for execution. The pyro.log_rotate method is used near the end to open log files at startup.

# Log file archival and pruning
method.insert = pyro._logfile_find_cmd, simple|private,\
     "cat = \"find \", (cfg.logs),\
            \" -daystart -type f -name '*.\", (argument.0),\"'\",\
            \" -mtime +\", (argument.1),\
            \" -exec nice \", (argument.2), \" '{}' ';'\""

schedule2 = pyro_logfile_archival, 01:10:00, 24:00:00,\
    "execute.nothrow = bash, -c, (pyro._logfile_find_cmd, log, (pyro.log_archival.days), gzip)"

schedule2 = pyro_logfile_pruning, 01:20:00, 24:00:00,\
    "execute.nothrow = bash, -c, (pyro._logfile_find_cmd, log.gz, (pyro.log_retention.days), rm)"

# Open logs initially on startup
pyro.log_rotate=

Rename Item Using its Tied-to File

The rename2tied.sh script overwrites an item’s name using the file name of its tied-to file, when you press the R key with a fresh unstarted item in focus.

This is useful when the metafile names generated by a tracker contain more useful information than the info.name of the metafile content. Also, those metafile names typically have a common format, which can help with properly organizing your downloads.

Warning

Right now, this only works for items that are not started yet, i.e. were added using load.normal and have no data files yet.

Also, the item needs to be loaded from a file, so there actually is a tied-to name – items loaded via ruTorrent do not have one!

Here is the core script code (minus some boilerplate):

hash="${1:?hash is missing}"
name="${2:?name is missing}"
path="${3}"
tied="${4}"
multi="${5:?is_multi_file is missing}"

fail() {
    msg="$(echo -n "$@")"
    rtxmlrpc print '' "ERROR: $msg [$name]"
    exit 1
}

test -n "$path" || fail "Empty directory"
test -n "$tied" || fail "Empty tied file"
test ! -e "$path/$name" || fail "Cannot rename an item with existing data"

tracker="$(rtcontrol --from-view $hash // -qo alias)"

# Build new name
new_name="${tied##*/}"  # Reduce path to basename
new_name="${new_name// /.}"  # Replace spaces with dots
new_name="${new_name%.torrent}"  # Remove extension
while test "$new_name" != "${new_name%[.0-9]}"; do
    new_name="${new_name%[.0-9]}"  # Remove trailing IDs etc.
done

# Remove bad directory name (that we want to replace) from multi-file item
new_full_path="${path%/}"
if test "$multi" -eq 1; then
    new_full_path="${new_full_path%/*}"
fi

# Remove common extensions
for ext in mkv mp4 m4v avi; do
    new_name="${new_name%.$ext}"
    new_name="${new_name%.$(tr a-z A-Z <<<$ext)}"
done

# Change source tags to encode tags (when item has an encoded media type)
if egrep -i >/dev/null '\.[xh]\.?264' <<<"$new_name"; then
    new_name=$(sed -re 's~\.DVD\.~.DVDRip.~' -e 's~\.Blu-ray\.~.BDRip.~' <<<"$new_name")
fi

# Add tracker as group if none is there
if ! egrep >/dev/null '.-[a-zA-Z0-9]+$' <<<"$new_name"; then
    new_name="${new_name}-$tracker"
fi

# Rename / relocate item
new_full_path="${new_full_path%/}/$new_name"
rtxmlrpc d.directory_base.set $hash "$new_full_path"
rtxmlrpc d.custom.set $hash displayname "$new_name"

Install the full script by calling these commands:

gh_raw="https://raw.githubusercontent.com/rtorrent-community/rtorrent-docs"
mkdir -p ~/rtorrent/scripts
wget $gh_raw/master/docs/examples/rename2tied.sh -O $_/rename2tied.sh
chmod a+rx $_

Note that you also must have pyrocore installed, so that the rtcontrol and rtxmlrpc commands are available.

This is the configuration snippet that binds calling the script to the R key. For key binding, you need rTorrent-PS though – otherwise leave out the pyro.bind_key command, and call pyro._rename2tied= via a Ctrl-X prompt.

method.insert = pyro._rename2tied, private|simple, \
    "execute.nothrow = ~/rtorrent/scripts/rename2tied.sh, \
     (d.hash), (d.name), (d.directory), (d.tied_to_file), (d.is_multi_file)"

pyro.bind_key = rename2tied, R, "pyro._rename2tied="

Depending on your needs, it can also make sense to call the script in an inserted_new event handler, or as a post-load command in a watch schedule. If you do that, you should probably add some checks that only apply changes for certain trackers, or when the tied-to file name has a certain format.

Versatile Move on Completion

The completion-path.sh script allows you to perform very versatile completion moving, based on logic defined in a bash script

Calling the script with -h prints full installation instructions including the rTorrent config snippet shown further below.

gh_raw="https://raw.githubusercontent.com/rtorrent-community/rtorrent-docs"
wget -O /tmp/completion-path.sh $gh_raw/master/docs/examples/completion-path.sh
bash /tmp/completion-path.sh -h

Read on to learn how this works when added to your rTorrent instance.

The target path is determined in the set_target_path function at the top of the script:

    local month=$(date +'%Y-%m')

    # Only move data downloaded into a "work" directory
    egrep >/dev/null "/work/" <<<"${base_path}/" || return

    # "target_base" is used to complete a non-empty but relative "target" path
    target_base=$(sed -re 's~^(.*)/work/.*~\1/done~' <<<"${base_path}")
    target_tail=$(sed -re 's~^.*/work/(.*)~\1~' <<<"${base_path}")
    test "$is_multi_file" -eq 1 || target_tail="$(dirname "$target_tail")"
    test "$target_tail" != '.' || target_tail=""

    # Move by label
    test -n "$target" || case $(tr A-Z' ' a-z_ <<<"${label:-NOT_SET}") in
        tv|hdtv)                    target="TV" ;;
        movie*)                     target="Movies/$month" ;;
    esac

    # Move by name patterns (check both displayname and info.name)
    for i in "$display_name" "$name"; do
        test -n "$target" -o -z "$i" || case $(tr A-Z' ' a-z. <<<"${i}") in
            *hdtv*|*pdtv*)              target="TV" ;;
            *.s[0-9][0-9].*)            target="TV" ;;
            *.s[0-9][0-9]e[0-9][0-9].*) target="TV" ;;
            *pdf|*epub|*ebook*)         target="eBooks/$month" ;;
        esac
    done

    test -z "$target" && is_movie "$name" && target="Movies/$month" || :
    test -z "$target" -a -n "$display_name" && is_movie "$display_name" && target="Movies/$month" || :

    # Append tail path if non-empty
    test -z "$target" -o -z "$target_tail" || target="$target/$target_tail"

Change it according to your preferences. If you don’t assign a value to target, the item is not moved and remains in its default download location for later manual moving.

The is_movie helper function uses an inline Python script to check for typical names of movie releases using a regular expression:

import re
import sys

pattern = re.compile(
    r"^(?P<title>.+?)[. ](?P<year>\d{4})"
    r"(?:[._ ](?P<release>UNRATED|REPACK|INTERNAL|PROPER|LIMITED|RERiP))*"
    r"(?:[._ ](?P<format>480p|576p|720p|1080p|1080i|2160p))?"
    r"(?:[._ ](?P<srctag>[a-z]{1,9}))?"
    r"(?:[._ ](?P<source>BDRip|BRRip|HDRip|DVDRip|DVD[59]?|PAL|NTSC|Web|WebRip|WEB-DL|Blu-ray|BluRay|BD25|BD50))"
    r"(?:[._ ](?P<sound1>MP3|DD.?[25]\.[01]|AC3|AAC(?:2.0)?|FLAC(?:2.0)?|DTS(?:-HD)?))?"
    r"(?:[._ ](?P<codec>xvid|divx|avc|x264|h\.?264|hevc|h\.?265))"
    r"(?:[._ ](?P<sound2>MP3|DD.?[25]\.[01]|AC3|AAC(?:2.0)?|FLAC(?:2.0)?|DTS(?:-HD)?))?"
    r"(?:[-.](?P<group>.+?))"
    r"(?P<extension>\.avi|\.mkv|\.mp4|\.m4v)?$", re.I
)

title = ' '.join(sys.argv[1:])
sys.exit(not pattern.match(title))

The is_movie check is done after the more reliable name checks.

For the script to be called and used as part of completion moving, these commands need to be added to your rtorrent.rc or config.d/move_on_completion.rc (see Load ‘Drop-In’ Config Fragments on how to get a config.d directory):

method.insert = completion_path, simple|private, "execute.capture = \
    ~/rtorrent/scripts/completion-path.sh, \
    (directory.default), (session.path), \
    (d.hash), (d.name), (d.directory), (d.base_path), (d.tied_to_file), \
    (d.is_multi_file), (d.custom1), (d.custom, displayname)"

method.insert = completion_dirname, simple|private, \
    "execute.capture = bash, -c, \"dirname \\\"$1\\\" | tr -d $'\\\\n'\", \
                             completion_dirname, (argument.0)"

method.insert = completion_move_print, simple|private, \
    "print = \"MOVED »\", (argument.0), \"« to »\", (argument.1), «"

method.insert = completion_move_single, simple|private, \
    "d.directory.set = (argument.1); \
     execute.throw = mkdir, -p, (argument.1); \
     execute.throw = mv, -u, (argument.0), (argument.1)"

method.insert = completion_move_multi, simple|private, \
    "d.directory_base.set = (argument.1); \
     execute.throw = mkdir, -p, (completion_dirname, (argument.1)); \
     execute.throw = mv, -uT, (argument.0), (argument.1)"

method.insert = completion_move, simple|private, \
    "branch=d.is_multi_file=, \
        \"completion_move_multi = (argument.0), (argument.1)\", \
        \"completion_move_single = (argument.0), (argument.1)\" ; \
     d.save_full_session="

method.insert = completion_move_verbose, simple|private, \
    "completion_move = (argument.0), (argument.1); \
     completion_move_print = (argument.0), (argument.1)"

method.insert = completion_move_handler, simple|private, \
    "branch=\"not=(equal, argument.0=, cat=)\", \
        \"completion_move_verbose = (d.base_path), (argument.0)\""

method.set_key = event.download.finished, move_on_completion, \
    "completion_move_handler = (completion_path)"

In the completion_move_handler method, you can change completion_move_verbose to just completion_move, if you don’t want the move logged.

The completion_path method already passes the major item attributes to the script. You can add more if you need to, but then you also need to extend the list of names in arglist at the top of the bash script.

arglist=( default session hash name directory base_path tied_to_file is_multi_file label display_name )

If you run rTorrent-PS, which has the d.tracker_domain command, you can use that command to add a rule for trackers dedicated to one specific content type. Extend the last line of completion_path to read …displayname), (d.tracker_domain)", and add tracker_domain to the end of arglist. Then add a rule like this to the body of set_target_path:

# Move by tracker
test -n "$target" || case $(tr A-Z' ' a-z_ <<<"${tracker_domain:-NOT_SET}") in
    linuxtracker.org) target="Software" ;;
esac

Delayed Completion Handling

The following config snippet defines a new event.download.finished_delayed trigger that works like the normal finished event, but only fires after a customizable delay.

One use-case for such a thing is to move a download from fast storage (RAM disk, SSD) to slow storage (HDD) for permanent seeding, after the initial rush in a swarm is over.

The following is the config you need to add to a config.d/event.download.finished_delayed.rc file (see Load ‘Drop-In’ Config Fragments on how to get a config.d directory), or else to your normal rtorrent.rc file:

#############################################################################
# Add a "finished_delayed" event
#
# See https://github.com/rakshasa/rtorrent/issues/547
#############################################################################

# Delay in seconds
method.insert.value = event.download.finished_delayed.interval, 600

# Add persistent view (queue holding delayed items)
view.add = finished_delayed
view.persistent = finished_delayed

# Add new event for delayed completion handling
method.insert = event.download.finished_delayed, multi|rlookup|static
method.set_key = event.download.finished, !add_to_finished_delayed, \
    "d.views.push_back_unique = finished_delayed ; \
     view.filter_download = finished_delayed"
method.set_key = event.download.finished_delayed, !remove_from_finished_delayed, \
    "d.views.remove = finished_delayed ; \
     view.filter_download = finished_delayed"

# Call new event for items that passed the delay interval
schedule2 = event.download.finished_delayed, 60, 60, \
    ((d.multicall2, finished_delayed, \
        "branch=\"elapsed.greater=(d.timestamp.finished),(event.download.finished_delayed.interval)\", \
            event.download.finished_delayed="))

# For debugging…
method.set_key = event.download.finished_delayed, !debug, \
    "print = \"DELAYED FINISH after \", (convert.elapsed_time, (d.timestamp.finished)), \
             \" of \", (d.name)"

The last command adding a !debug handler can be left out, if you want less verbosity.

Set a Download to “Seed Only”

The d.seed_only command helps you to stop all download activity on an item. Select any unfinished item, press Ctrl-X, and enter d.seed_only= followed by . Then all files in that item are set to off, and any peers still sending you data are cut off. The data you have is still seeded, as long as the item is not stopped.

method.insert = d.seed_only, private|simple,\
    "f.multicall = *, f.priority.set=0 ;\
     d.update_priorities= ;\
     d.disconnect.seeders="

f.multicall calls f.priority.set on every file, d.update_priorities makes these changes known, and finally d.disconnect.seeders kicks any active seeders.

Scheduled Bandwidth Shaping

This example shows how to use schedule2 with absolute start times, to set the download rate depending on the wall clock time, at 10AM and 4PM. The result is a very simple form of bandwidth shaping, with full speed transfers enabled while you’re at work (about 16 MiB/s in the example), and only very moderate bandwidth usage when you’re at home.

schedule2 = throttle_full, 10:00:00, 24:00:00, ((throttle.global_down.max_rate.set_kb, 16000))
schedule2 = throttle_slow, 16:00:00, 24:00:00, ((throttle.global_down.max_rate.set_kb,  1000))

Use throttle.global_up.max_rate.set_kb for setting the upload rate.

If you call these commands via XMLRPC from an outside script, you can implement more complex rules, e.g. throttling when other computers are visible on the network.

External scripts should also be used when saving money is the goal, in cases where you have to live with disadvantageous ISP plans with bandwidth caps. Run such a script very regularly (via cron), to enforce the bandwidth rules continuously.