Posts tagged with 'Ruby'

A Robust Reporting System for Ledger

Sunday, 1 January around 10 o'clock am

Note: you can find much more information about ledger on ledger-cli.org, including links to official documentation and other implementations

For the last five years I've kept my personal finances in order using the ledger system, a sophisticated command line program that consumes a lightly formatted text file. It's helped me repay debts and get everything in order, helping me financially absorb an injury last month that would have been extremely detrimental just a few years prior.

The stock ledger program is exclusively command-line oriented. For quick checks and greping over output, this is fine. For some time, though, I've wanted a more graphical, more robust way of looking at my finances. I've also wanted a more familiar query language, since version 2.0's queries were someone limited and version 3.0's query syntax is not very well documented yet. Last year I wrote a simple system that pushed monthly reports out to static HTML files, which got me part of the way there but I really wanted something more flexible. Something where I can just write an arbitrary query and have the output dumped to HTML.

Thus, I present Ledger Web. In Ledger Web, your ledger is kept in a text file, just the same as always, reports are ERB files, and queries are SQL. Ledger Web watches your ledger file and whenever it changes dumps it into a PostgreSQL database table. It's also quite customizable, letting you set hooks both before and after row insertion and before and after ledger file load.

Tagged: Personal Finance  Ledger  Projects  Ruby 

Read More -

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 -