Skip to content

Instantly share code, notes, and snippets.

@ohac
Created March 8, 2010 12:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save ohac/325107 to your computer and use it in GitHub Desktop.
Save ohac/325107 to your computer and use it in GitHub Desktop.
transmission-remote for ruby
#!/usr/bin/ruby
require 'rubygems'
require 'nokogiri'
require 'net/http'
require 'json'
require 'timeout'
require 'fileutils'
require 'open-uri'
require 'base64'
require 'choice'
class Transmission
def initialize(host, port, user = nil, pass = nil)
@host = host
@port = port
@user = user
@pass = pass
end
def list
sessionid = Net::HTTP.start(@host, @port) do |http|
res = http.get('/transmission/rpc')
h = Nokogiri::HTML.parse(res.body)
h.css('code').text.split.last
end
header = {
'Content-Type' => 'application/json',
}
header['X-Transmission-Session-Id'] = sessionid unless sessionid.nil?
sessionid = Net::HTTP.start(@host, @port) do |http|
json = {
:method => 'torrent-get',
:arguments => { :fields => [ :hashString, :id, :name ] }
}
res = http.post('/transmission/rpc', json.to_json, header)
JSON.parse(res.body)
end
end
# TODO DRY
def add(torrent)
sessionid = Net::HTTP.start(@host, @port) do |http|
res = http.get('/transmission/rpc')
h = Nokogiri::HTML.parse(res.body)
h.css('code').text.split.last
end
header = {
'Content-Type' => 'application/json',
}
header['X-Transmission-Session-Id'] = sessionid unless sessionid.nil?
Net::HTTP.start(@host, @port) do |http|
json = {
:method => 'torrent-add',
:arguments => { :metainfo => Base64::encode64(torrent) }
}
http.post('/transmission/rpc', json.to_json, header)
end
end
end
class UTorrent
def initialize(host, port, user = nil, pass = nil)
@host = host
@port = port
@user = user
@pass = pass
end
def list
Net::HTTP.start(@host, @port) do |http|
req = Net::HTTP::Get.new('/gui/token.html')
req.basic_auth @user, @pass
res = http.request(req)
h = Nokogiri::HTML.parse(res.body)
token = h.css('#token').text
req = Net::HTTP::Get.new('/gui/?list=1&token=%s' % token)
req.basic_auth @user, @pass
res = http.request(req)
result = JSON.parse(res.body)
transmissionlike = result['torrents'].map do |t|
{ 'hashString' => t[0].downcase, 'name' => t[2] }
end
{ 'arguments' => { 'torrents' => transmissionlike } }
end
end
# TODO DRY
def add(torrent)
Net::HTTP.start(@host, @port) do |http|
req = Net::HTTP::Get.new('/gui/token.html')
req.basic_auth @user, @pass
res = http.request(req)
h = Nokogiri::HTML.parse(res.body)
token = h.css('#token').text
req = Net::HTTP::Post.new('/gui/?action=add-file&token=%s' % token)
req.basic_auth @user, @pass
req.set_content_type('multipart/form-data; boundary=myboundary')
req.body = <<EOF
--myboundary\r
Content-Disposition: form-data; name="torrent_file"\r
Content-Type: application/octet-stream\r
Content-Transfer-Encoding: binary\r
\r
#{torrent}\r
--myboundary--\r
EOF
http.request(req)
end
end
end
HOME_DIR = ENV['HOME']
SETTING_DIR = "#{HOME_DIR}/.torrentsync"
PEERS_FILE = File.join(SETTING_DIR, 'peers')
TORRENTS_FILE = File.join(SETTING_DIR, 'torrents')
unless File.exist?(SETTING_DIR)
FileUtils.mkdir SETTING_DIR
open(PEERS_FILE, 'w') do |fd|
fd.puts('transmission localhost 9091')
end
open(TORRENTS_FILE, 'w') do |fd|
fd.puts("file:#{HOME_DIR}/.config/transmission/torrents")
end
end
def find_torrent(name)
uris = File.open(TORRENTS_FILE).readlines.map(&:chomp).map{|u| URI.parse(u)}
rv = nil
uris.each do |uri|
ts = case uri.scheme
when 'file'
Dir.glob(File.join(uri.path, '*.torrent')).map{|t|File.basename(t)}
when 'http'
body = open(uri).read
h = Nokogiri::HTML.parse(body)
h.css('a').map{|a| a.text}.select{|t| /\.torrent$/ === t}
else
raise
end
rp = ts.find{|t| !t.index(name).nil?}
next if rp.nil?
uri2 = URI.parse(URI.encode("#{uri.to_s}/#{rp}"))
rv = case uri2.scheme
when 'file'
open(uri2.path){|f|f.read}
when 'http'
open(uri2){|f|f.read}
end
# TODO need to check info_hash too
break
end
rv
end
def type2class(type)
case type
when 'transmission'
Transmission
when 'utorrent'
UTorrent
end
end
def get_peers
File.open(PEERS_FILE).readlines.map(&:chomp).map(&:split)
end
def get_torrents(peers)
torrents = {}
dead_peers = []
peers.each do |peer|
type = peer[0]
next if type[0, 1] == '#'
host, port, user, pass = peer[1], peer[2].to_i, peer[3], peer[4]
tr = begin
timeout(2) do
type2class(type).new(host, port, user, pass).list
end
rescue TimeoutError, Errno::ECONNREFUSED
dead_peers << peer
nil
end
next if tr.nil?
tr['arguments']['torrents'].each do |t|
h = t['hashString']
torrents[h] = { :name => t['name'], :peers => [] } unless torrents.key?(h)
torrents[h][:peers] << [host, port].join(':')
end
end
[torrents, dead_peers]
end
def sync_torrents(peers, torrents)
torrents.each do |hash, t|
name = t[:name]
hps = t[:peers]
next if hps.size >= 2
body = find_torrent(name)
next if body.nil?
hps = hps.map{|hp| host, port = hp.split(':'); [host, port.to_i]}
dests = peers.select do |peer|
hps.any?{|hp| peer[1] != hp[0] && peer[2] != hp[1]}
end
dest = dests.shuffle.first
puts "mirroring: %s to %s" % [name, dest.join(',')]
type = dest[0]
host, port, user, pass = dest[1], dest[2].to_i, dest[3], dest[4]
dest = type2class(type).new(host, port, user, pass)
dest.add(body)
end
end
Choice.options do
option :sync do
short '-s'
end
option :html do
short '-h'
end
end
c = Choice.choices
if c.sync
peers = get_peers
torrents = get_torrents(peers)
sync_torrents(peers, torrents)
end
if c.html
peers = get_peers
torrents, dead_peers = get_torrents(peers)
puts "<html>"
puts "<body>"
puts "<table>"
puts "<tr><th>Client</th><th>Host</th><th>Port</th><th>Dead?</th></tr>"
peers.each do |peer|
dead = dead_peers.include? peer
puts "<tr><td>#{peer[0]}</td><td>#{peer[1]}</td><td>#{peer[2]}</td><td>#{dead}</td></tr>"
end
puts "</table>"
puts "<table>"
puts "<tr><th>Hash</th><th>Name</th><th>Peers</th></tr>"
torrents.each do |hash, t|
puts "<tr><td>#{hash}</td><td>#{t[:name]}</td><td>#{t[:peers].size}</td></tr>"
end
puts "</table>"
puts "</body>"
puts "</html>"
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment