Posts tagged with 'BitTorrent'

Using the Transmission RPC Interface to Organize Torrents

Saturday, 27 November around 6 o'clock pm

I've been using my Mac mini as a torrent downloader and media server for a few years now using Transmission. Tonight I got fed up with my torrents being in one unsorted mass so I learned the HTTP RPC api and wrote a simple blocking client that organizes them.

I tried out an existing client that I found on GitHub, but it was sort of difficult to use, involving EventMachine and evented techniques. If this was part of a long-running processes I would spend the time to learn how to do this stuff, but this is just a simple script so a blocking technique is just fine.

Here's the client class:

require 'rubygems'
require 'mechanize'
require 'json'

class TransmissionClient
  attr_reader :agent,
              :header_name,
              :header_val,
              :password,
              :port,
              :server,
              :username

  def initialize(params)
    @username = params['username']
    @password = params['password']
    @server   = params['server']
    @port     = params['port'] || "9091"

    @agent = Mechanize.new

    @agent.auth(username, password)

    @header_name = ""
    @header_val = ""
  end

  def server_uri
    return "http://#{server}:#{port}/transmission/rpc"
  end

  def send(method, params)
    resp = ""

    begin 
      resp = agent.post(
        server_uri,
        JSON.generate({
          "method" => method,
          "arguments" => params
        }),
        header_name => header_val
      )

      resp_obj = JSON.parse(resp.body)

      if resp_obj["result"] == "success" then
        return resp_obj["arguments"]
      else
        raise RuntimeError resp.body
      end

    rescue Mechanize::ResponseCodeError => e
      @header_name, @header_val =
          e.page.search("code").first.content.split(/: /)
      retry
    end
  end
end

The only real surprise here is that Transmission sends a session ID in an error response (http code 409) and expects that session ID to be sent as a header in every request thereafter. Because of how the ruby version of Mechanize works, it's hard to get the headers from the exception that gets thrown on errors, so instead I extract it directly from the response page. Conveniently the page property on the exception acts like a nokogiri object so I can just search for the single code block on the page and grab the conveniently formatted header.

Here's some code that uses the above class to relocate torrents into folders:

def get_folder_from_name(name)
  name_parts = name.split(/\./)
  series_parts = []

  if name_parts[0].match(/^[0-9]+/) then
    series_parts.push name_parts.shift
  end

  name_parts.each do |p|
    break if p.match(/(s?)\d/i)
    series_parts.push p
  end

  if series_parts.length > 0 then
    return series_parts.map{|s| s.downcase}
                       .join("_")
                       .gsub("aaf-", "")
  else
    return nil
  end
end

server_info_str = IO.read("/Users/Peter/.transmission_server");
server_info = JSON.parse(server_info_str)

client = TransmissionClient.new(server_info)

torrents = client.send(
  "torrent-get",
  "fields" => %w{id name downloadDir}
);

torrents["torrents"].each do |t|
  series_name = get_folder_from_name(t["name"])
  next unless series_name

  name = t['name']
  id   = t['id']
  download_dir = t['download_dir']

  puts "Moving #{name} (#{id}) from #{download_dir} to #{series_name}";

  client.send(
    "torrent-set-location", 
    "ids"      => t["id"], 
    "location" => "/Users/Peter/Movies/#{series_name}",
    "move"     => true
  )
end

This instanciates a new client and grabs the id, name, and downloadDir fields for each currently active torrent. Then, it extracts a folder name from torrent's name and tells the client to move the torrent into the proper location. The paths are hardcoded because as I said before, this is a pretty stupid simple script. There's no reason they couldn't be in the config file that holds the server info. Also, the function that extracts a folder name is very specific to the torrents I'm working with.

Tagged: Programming  Ruby  BitTorrent 

Read More -